home *** CD-ROM | disk | FTP | other *** search
/ PC Open 107 / PC Open 107 CD 1.bin / CD1 / INTERNET / EMAIL / pop file / setup.exe / Classifier / Bayes.pm next >
Encoding:
Perl POD Document  |  2004-12-15  |  131.1 KB  |  3,941 lines

  1. # POPFILE LOADABLE MODULE
  2. package Classifier::Bayes;
  3.  
  4. use POPFile::Module;
  5. @ISA = ("POPFile::Module");
  6.  
  7. #----------------------------------------------------------------------------
  8. #
  9. # Bayes.pm --- Naive Bayes text classifier
  10. #
  11. # Copyright (c) 2001-2004 John Graham-Cumming
  12. #
  13. #   This file is part of POPFile
  14. #
  15. #   POPFile is free software; you can redistribute it and/or modify
  16. #   it under the terms of the GNU General Public License as published by
  17. #   the Free Software Foundation; either version 2 of the License, or
  18. #   (at your option) any later version.
  19. #
  20. #   POPFile is distributed in the hope that it will be useful,
  21. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  23. #   GNU General Public License for more details.
  24. #
  25. #   You should have received a copy of the GNU General Public License
  26. #   along with POPFile; if not, write to the Free Software
  27. #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  28. #
  29. #   Modified by              Sam Schinke    (sschinke@users.sourceforge.net)
  30. #   Merged with db code from Scott Leighton (helphand@users.sourceforge.net)
  31. #
  32. #----------------------------------------------------------------------------
  33.  
  34. use strict;
  35. use warnings;
  36. use locale;
  37. use Classifier::MailParse;
  38. use IO::Handle;
  39. use DBI;
  40. use Digest::MD5 qw( md5_hex );
  41. use MIME::Base64;
  42. use File::Copy;
  43.  
  44. # This is used to get the hostname of the current machine
  45. # in a cross platform way
  46.  
  47. use Sys::Hostname;
  48.  
  49. # A handy variable containing the value of an EOL for networks
  50.  
  51. my $eol = "\015\012";
  52.  
  53. # Korean characters definition
  54.  
  55. my $ksc5601_sym = '(?:[\xA1-\xAC][\xA1-\xFE])';
  56. my $ksc5601_han = '(?:[\xB0-\xC8][\xA1-\xFE])';
  57. my $ksc5601_hanja  = '(?:[\xCA-\xFD][\xA1-\xFE])';
  58. my $ksc5601 = "(?:$ksc5601_sym|$ksc5601_han|$ksc5601_hanja)";
  59.  
  60. my $eksc = "(?:$ksc5601|[\x81-\xC6][\x41-\xFE])"; #extended ksc
  61.  
  62. #----------------------------------------------------------------------------
  63. # new
  64. #
  65. #   Class new() function
  66. #----------------------------------------------------------------------------
  67. sub new
  68. {
  69.     my $type = shift;
  70.     my $self = POPFile::Module->new();
  71.  
  72.     # Set this to 1 to get scores for individual words in message detail
  73.  
  74.     $self->{wordscores__}        = 0;
  75.  
  76.     # Choice for the format of the "word matrix" display.
  77.  
  78.     $self->{wmformat__}          = '';
  79.  
  80.     # Just our hostname
  81.  
  82.     $self->{hostname__}        = '';
  83.  
  84.     # File Handle for DBI database
  85.  
  86.     $self->{db__}                = {};
  87.  
  88.     $self->{history__}        = 0;
  89.  
  90.     # To save time we also 'prepare' some commonly used SQL statements
  91.     # and cache them here, see the function db_connect__ for details
  92.  
  93.     $self->{db_get_buckets__} = 0;
  94.     $self->{db_get_wordid__} = 0;
  95.     $self->{db_get_word_count__} = 0;
  96.     $self->{db_put_word_count__} = 0;
  97.     $self->{db_get_bucket_unique_counts__} = 0;
  98.     $self->{db_get_unique_word_count__} = 0;
  99.     $self->{db_get_bucket_word_counts__} = 0;
  100.     $self->{db_get_full_total__} = 0;
  101.     $self->{db_get_bucket_parameter__} = 0;
  102.     $self->{db_set_bucket_parameter__} = 0;
  103.     $self->{db_get_bucket_parameter_default__} = 0;
  104.     $self->{db_get_buckets_with_magnets__} = 0;
  105.     $self->{db_delete_zero_words__} = 0;
  106.  
  107.     # Caches the name of each bucket and relates it to both the bucket
  108.     # ID in the database and whether it is pseudo or not
  109.     #
  110.     # Subkeys used are:
  111.     #
  112.     # id     The bucket ID in the database
  113.     # pseudo 1 if this is a pseudo bucket
  114.  
  115.     $self->{db_bucketid__}       = {};
  116.  
  117.     # Caches the IDs that map to parameter types
  118.  
  119.     $self->{db_parameterid__}    = {};
  120.  
  121.     # Caches looked up parameter values on a per bucket basis
  122.  
  123.     $self->{db_parameters__}     = {};
  124.  
  125.     # Used to parse mail messages
  126.     $self->{parser__}            = new Classifier::MailParse;
  127.  
  128.     # The possible colors for buckets
  129.     $self->{possible_colors__} = [ 'red',       'green',      'blue',       'brown', # PROFILE BLOCK START
  130.                                    'orange',    'purple',     'magenta',    'gray',
  131.                                    'plum',      'silver',     'pink',       'lightgreen',
  132.                                    'lightblue', 'lightcyan',  'lightcoral', 'lightsalmon',
  133.                                    'lightgrey', 'darkorange', 'darkcyan',   'feldspar',
  134.                                    'black' ];                                        # PROFILE BLOCK STOP
  135.  
  136.     # Precomputed per bucket probabilities
  137.     $self->{bucket_start__}      = {};
  138.  
  139.     # A very unlikely word
  140.     $self->{not_likely__}        = {};
  141.  
  142.     # The expected corpus version
  143.     #
  144.     # DEPRECATED  This is only used when upgrading old flat file corpus files
  145.     #             to the database
  146.     $self->{corpus_version__}    = 1;
  147.  
  148.     # The unclassified cutoff this value means that the top
  149.     # probabilily must be n times greater than the second probability,
  150.     # default is 100 times more likely
  151.     $self->{unclassified__}      = log(100);
  152.  
  153.     # Used to tell the caller whether a magnet was used in the last
  154.     # mail classification
  155.     $self->{magnet_used__}       = 0;
  156.     $self->{magnet_detail__}     = 0;
  157.  
  158.     # This maps session keys (long strings) to user ids.  If there's
  159.     # an entry here then the session key is valid and can be used in
  160.     # the POPFile API.  See the methods get_session_key and
  161.     # release_session_key for details
  162.  
  163.     $self->{api_sessions__}      = {};
  164.  
  165.     # Used to indicate whether we are using SQLite and what the full
  166.     # path and name of the database is if we are.
  167.  
  168.     $self->{db_is_sqlite__}      = 0;
  169.     $self->{db_name__}           = '';
  170.  
  171.     # Must call bless before attempting to call any methods
  172.  
  173.     bless $self, $type;
  174.  
  175.     $self->name( 'bayes' );
  176.  
  177.     return $self;
  178. }
  179.  
  180. #----------------------------------------------------------------------------
  181. #
  182. # forked
  183. #
  184. # This is called inside a child process that has just forked, since
  185. # the child needs access to the database we open it
  186. #
  187. #----------------------------------------------------------------------------
  188. sub forked
  189. {
  190.     my ( $self ) = @_;
  191.  
  192.     $self->db_connect__();
  193. }
  194.  
  195. #----------------------------------------------------------------------------
  196. #
  197. # initialize
  198. #
  199. # Called to set up the Bayes module's parameters
  200. #
  201. #----------------------------------------------------------------------------
  202. sub initialize
  203. {
  204.     my ( $self ) = @_;
  205.  
  206.     # This is the name for the database
  207.  
  208.     $self->config_( 'database', 'popfile.db' );
  209.  
  210.     # This is the 'connect' string used by DBI to connect to the
  211.     # database, if you decide to change from using SQLite to some
  212.     # other database (e.g. MySQL, Oracle, ... ) this *should* be all
  213.     # you need to change.  The additional parameters user and auth are
  214.     # needed for some databases. 
  215.     #
  216.     # Note that the dbconnect string
  217.     # will be interpolated before being passed to DBI and the variable
  218.     # $dbname can be used within it and it resolves to the full path
  219.     # to the database named in the database parameter above.
  220.  
  221.     $self->config_( 'dbconnect', 'dbi:SQLite:dbname=$dbname' );
  222.     $self->config_( 'dbuser', '' ); $self->config_( 'dbauth', '' );
  223.  
  224.     # SQLite 1.05+ have some problems we are resolving.  This lets us
  225.     # give a nice message and then disable the version checking later
  226.     
  227.     $self->config_( 'bad_sqlite_version', '3.0.0' );
  228.  
  229.     # No default unclassified weight is the number of times more sure
  230.     # POPFile must be of the top class vs the second class, default is
  231.     # 100 times more
  232.  
  233.     $self->config_( 'unclassified_weight', 100 );
  234.  
  235.     # The corpus is kept in the 'corpus' subfolder of POPFile
  236.     #
  237.     # DEPRECATED This is only used to find an old corpus that might
  238.     # need to be upgraded
  239.  
  240.     $self->config_( 'corpus', 'corpus' );
  241.  
  242.     # The characters that appear before and after a subject
  243.     # modification
  244.  
  245.     $self->config_( 'subject_mod_left',  '[' );
  246.     $self->config_( 'subject_mod_right', ']' );
  247.  
  248.     # Get the hostname for use in the X-POPFile-Link header
  249.  
  250.     $self->{hostname__} = hostname;
  251.  
  252.     # Allow the user to override the hostname
  253.  
  254.     $self->config_( 'hostname', $self->{hostname__} );
  255.  
  256.     # If set to 1 then the X-POPFile-Link will have < > around the URL
  257.     # (i.e. X-POPFile-Link: <http://foo.bar>) when set to 0 there are
  258.     # none (i.e. X-POPFile-Link: http://foo.bar)
  259.  
  260.     $self->config_( 'xpl_angle', 0 );
  261.  
  262.     # This is a bit mask used to control options when we are using the
  263.     # default SQLite database.  By default all the options are on.
  264.     #
  265.     # 1 = Asynchronous deletes
  266.     # 2 = Backup database every hour
  267.     
  268.     $self->config_( 'sqlite_tweaks', 0xFFFFFFFF );
  269.  
  270.     $self->mq_register_( 'COMIT', $self );
  271.     $self->mq_register_( 'RELSE', $self );
  272.  
  273.     # Register for the TICKD message which is sent hourly by the
  274.     # Logger module.  We use this to hourly save the database if bit 1
  275.     # of the sqlite_tweaks is set and we are using SQLite
  276.  
  277.     $self->mq_register_( 'TICKD', $self );
  278.  
  279.     return 1;
  280. }
  281.  
  282. #----------------------------------------------------------------------------
  283. #
  284. # deliver
  285. #
  286. # Called by the message queue to deliver a message
  287. #
  288. # There is no return value from this method
  289. #
  290. #----------------------------------------------------------------------------
  291. sub deliver
  292. {
  293.     my ( $self, $type, @message ) = @_;
  294.  
  295.     if ( $type eq 'COMIT' ) {
  296.         $self->classified( $message[0], $message[2] );
  297.     }
  298.     
  299.     if ( $type eq 'RELSE' ) {
  300.         $self->release_session_key_private__( $message[0] );
  301.     }    
  302.  
  303.     if ( $type eq 'TICKD' ) {
  304.         $self->backup_database__();
  305.     }
  306. }
  307.  
  308. #----------------------------------------------------------------------------
  309. #
  310. # start
  311. #
  312. # Called to start the Bayes module running
  313. #
  314. #----------------------------------------------------------------------------
  315. sub start
  316. {
  317.     my ( $self ) = @_;
  318.  
  319.     # In Japanese or Korean mode, explicitly set LC_COLLATE to C.
  320.     #
  321.     # This is to avoid Perl crash on Windows because default
  322.     # LC_COLLATE of Japanese Win is Japanese_Japan.932(Shift_JIS),
  323.     # which is different from the charset POPFile uses for Japanese
  324.     # characters(EUC-JP).
  325.  
  326.     if ( defined( $self->module_config_( 'html', 'language' ) ) &&
  327.        ( $self->module_config_( 'html', 'language' ) =~ /^Nihongo|Korean$/ )) {
  328.         use POSIX qw( locale_h );
  329.         setlocale( LC_COLLATE, 'C' );
  330.     }
  331.  
  332.     # Pass in the current interface language for language specific parsing
  333.  
  334.     $self->{parser__}->{lang__}  = $self->module_config_( 'html', 'language' );
  335.     $self->{unclassified__} = log( $self->config_( 'unclassified_weight' ) );
  336.  
  337.     if ( !$self->db_connect__() ) {
  338.         return 0;
  339.     }
  340.  
  341.     $self->upgrade_predatabase_data__();
  342.  
  343.     return 1;
  344. }
  345.  
  346. #----------------------------------------------------------------------------
  347. #
  348. # stop
  349. #
  350. # Called when POPFile is terminating
  351. #
  352. #----------------------------------------------------------------------------
  353. sub stop
  354. {
  355.     my ( $self ) = @_;
  356.  
  357.     $self->db_disconnect__();
  358.     delete $self->{parser__};
  359. }
  360.  
  361. #----------------------------------------------------------------------------
  362. #
  363. # classified
  364. #
  365. # Called to inform the module about a classification event
  366. #
  367. # There is no return value from this method
  368. #
  369. #----------------------------------------------------------------------------
  370. sub classified
  371. {
  372.     my ( $self, $session, $class ) = @_;
  373.  
  374.     $self->set_bucket_parameter( $session, $class, 'count',             # PROFILE BLOCK START
  375.         $self->get_bucket_parameter( $session, $class, 'count' ) + 1 ); # PROFILE BLOCK STOP
  376. }
  377.  
  378. #----------------------------------------------------------------------------
  379. #
  380. # backup_database__
  381. #
  382. # Called when the TICKD message is received each hour and if we are using
  383. # the default SQLite database will make a copy with the .backup extension
  384. #
  385. #----------------------------------------------------------------------------
  386. sub backup_database__
  387. {
  388.     my ( $self ) = @_;
  389.  
  390.     # If database backup is turned on and we are using SQLite then
  391.     # backup the database by copying it
  392.  
  393.     if ( ( $self->config_( 'sqlite_tweaks' ) & 2 ) && 
  394.          $self->{db_is_sqlite__} ) {
  395.         if ( !copy( $self->{db_name__}, $self->{db_name__} . ".backup" ) ) {
  396.         $self->log_( 0, "Failed to backup database ".$self->{db_name__} );
  397.         }
  398.     }
  399. }
  400.  
  401. #----------------------------------------------------------------------------
  402. #
  403. # tweak_sqlite
  404. #
  405. # Called when a module wants is to tweak access to the SQLite database.
  406. #
  407. # $tweak    The tweak to apply (a bit in the sqlite_tweaks mask)
  408. # $state    1 to enable the tweak, 0 to disable
  409. # $db       The db handle to tweak
  410. #
  411. #----------------------------------------------------------------------------
  412. sub tweak_sqlite
  413. {
  414.     my ( $self, $tweak, $state, $db ) = @_;
  415.  
  416.     if ( $self->{db_is_sqlite__} && 
  417.          ( $self->config_( 'sqlite_tweaks' ) & $tweak ) ) {
  418.  
  419.         $self->log_( 1, "Performing tweak $tweak to $state" );
  420.  
  421.         if ( $tweak == 1 ) {
  422.             my $sync = $state?'off':'normal';
  423.             $db->do( "pragma synchronous=$sync;" );
  424.         }    
  425.     }
  426. }
  427.  
  428. #----------------------------------------------------------------------------
  429. #
  430. # reclassified
  431. #
  432. # Called to inform the module about a reclassification from one bucket
  433. # to another
  434. #
  435. # session            Valid API session
  436. # bucket             The old bucket name
  437. # newbucket          The new bucket name
  438. # undo               1 if this is an undo operation
  439. #
  440. # There is no return value from this method
  441. #
  442. #----------------------------------------------------------------------------
  443. sub reclassified
  444. {
  445.     my ( $self, $session, $bucket, $newbucket, $undo ) = @_;
  446.  
  447.     $self->log_( 0, "Reclassification from $bucket to $newbucket" );
  448.  
  449.     my $c = $undo?-1:1;
  450.  
  451.     if ( $bucket ne $newbucket ) {
  452.         my $count = $self->get_bucket_parameter(
  453.                         $session, $newbucket, 'count' );
  454.         my $newcount = $count + $c;
  455.         $newcount = 0 if ( $newcount < 0 );
  456.         $self->set_bucket_parameter(
  457.             $session, $newbucket, 'count', $newcount );
  458.  
  459.         $count = $self->get_bucket_parameter(
  460.                      $session, $bucket, 'count' );
  461.         $newcount = $count - $c;
  462.         $newcount = 0 if ( $newcount < 0 );
  463.         $self->set_bucket_parameter(
  464.             $session, $bucket, 'count', $newcount );
  465.  
  466.         my $fncount = $self->get_bucket_parameter(
  467.                           $session, $newbucket, 'fncount' );
  468.         my $newfncount = $fncount + $c;
  469.         $newfncount = 0 if ( $newfncount < 0 );
  470.         $self->set_bucket_parameter(
  471.             $session, $newbucket, 'fncount', $newfncount );
  472.  
  473.         my $fpcount = $self->get_bucket_parameter(
  474.                           $session, $bucket, 'fpcount' );
  475.         my $newfpcount = $fpcount + $c;
  476.         $newfpcount = 0 if ( $newfpcount < 0 );
  477.         $self->set_bucket_parameter(
  478.             $session, $bucket, 'fpcount', $newfpcount );
  479.     }
  480. }
  481.  
  482. #----------------------------------------------------------------------------
  483. #
  484. # get_color
  485. #
  486. # Retrieves the color for a specific word, color is the most likely bucket
  487. #
  488. # $session  Session key returned by get_session_key
  489. # $word     Word to get the color of
  490. #
  491. #----------------------------------------------------------------------------
  492. sub get_color
  493. {
  494.     my ( $self, $session, $word ) = @_;
  495.  
  496.     my $max   = -10000;
  497.     my $color = 'black';
  498.  
  499.     for my $bucket ($self->get_buckets( $session )) {
  500.         my $prob = $self->get_value_( $session, $bucket, $word );
  501.  
  502.         if ( $prob != 0 )  {
  503.             if ( $prob > $max )  {
  504.                 $max   = $prob;
  505.                 $color = $self->get_bucket_parameter( $session, $bucket,
  506.                              'color' );
  507.             }
  508.         }
  509.     }
  510.  
  511.     return $color;
  512. }
  513.  
  514. #----------------------------------------------------------------------------
  515. #
  516. # get_not_likely_
  517. #
  518. # Returns the probability of a word that doesn't appear
  519. #
  520. #----------------------------------------------------------------------------
  521. sub get_not_likely_
  522. {
  523.     my ( $self, $session ) = @_;
  524.  
  525.     my $userid = $self->valid_session_key__( $session );
  526.     return undef if ( !defined( $userid ) );
  527.  
  528.     return $self->{not_likely__}{$userid};
  529. }
  530.  
  531. #----------------------------------------------------------------------------
  532. #
  533. # get_value_
  534. #
  535. # Returns the value for a specific word in a bucket.  The word is
  536. # converted to the log value of the probability before return to get
  537. # the raw value just hit the hash directly or call get_base_value_
  538. #
  539. #----------------------------------------------------------------------------
  540. sub get_value_
  541. {
  542.     my ( $self, $session, $bucket, $word ) = @_;
  543.  
  544.     my $value = $self->db_get_word_count__( $session, $bucket, $word );
  545.  
  546.     if ( defined( $value ) && ( $value > 0 ) ) {
  547.  
  548.         # Profiling notes:
  549.         #
  550.         # I tried caching the log of the total value and then doing
  551.         # log( $value ) - $cached and this turned out to be
  552.         # much slower than this single log with a division in it
  553.  
  554.         return log( $value /
  555.                     $self->get_bucket_word_count( $session, $bucket ) );
  556.     } else {
  557.         return 0;
  558.     }
  559. }
  560.  
  561. sub get_base_value_
  562. {
  563.     my ( $self, $session, $bucket, $word ) = @_;
  564.  
  565.     my $value = $self->db_get_word_count__( $session, $bucket, $word );
  566.  
  567.     if ( defined( $value ) ) {
  568.         return $value;
  569.     } else {
  570.         return 0;
  571.     }
  572. }
  573.  
  574. #----------------------------------------------------------------------------
  575. #
  576. # set_value_
  577. #
  578. # Sets the value for a word in a bucket and updates the total word
  579. # counts for the bucket and globally
  580. #
  581. #----------------------------------------------------------------------------
  582. sub set_value_
  583. {
  584.     my ( $self, $session, $bucket, $word, $value ) = @_;
  585.  
  586.     if ( $self->db_put_word_count__( $session, $bucket,
  587.              $word, $value ) == 1 ) {
  588.  
  589.         # If we set the word count to zero then clean it up by deleting the
  590.         # entry
  591.  
  592.         my $userid = $self->valid_session_key__( $session );
  593.         my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  594.         $self->{db_delete_zero_words__}->execute( $bucketid );
  595.  
  596.         return 1;
  597.     } else {
  598.         return 0;
  599.     }
  600. }
  601.  
  602. #----------------------------------------------------------------------------
  603. #
  604. # get_sort_value_ behaves the same as get_value_, except that it
  605. # returns not_likely__ rather than 0 if the word is not found.  This
  606. # makes its result more suitable as a sort key for bucket ranking.
  607. #
  608. #----------------------------------------------------------------------------
  609. sub get_sort_value_
  610. {
  611.     my ( $self, $session, $bucket, $word ) = @_;
  612.  
  613.     my $v = $self->get_value_( $session, $bucket, $word );
  614.  
  615.     if ( $v == 0 ) {
  616.  
  617.         my $userid = $self->valid_session_key__( $session );
  618.         return undef if ( !defined( $userid ) );
  619.  
  620.         return $self->{not_likely__}{$userid};
  621.     } else {
  622.         return $v;
  623.     }
  624. }
  625.  
  626. #----------------------------------------------------------------------------
  627. #
  628. # update_constants__
  629. #
  630. # Updates not_likely and bucket_start
  631. #
  632. #----------------------------------------------------------------------------
  633. sub update_constants__
  634. {
  635.     my ( $self, $session ) = @_;
  636.  
  637.     my $wc = $self->get_word_count( $session );
  638.  
  639.     my $userid = $self->valid_session_key__( $session );
  640.     return undef if ( !defined( $userid ) );
  641.  
  642.     if ( $wc > 0 )  {
  643.         $self->{not_likely__}{$userid} = -log( 10 * $wc );
  644.  
  645.         foreach my $bucket ($self->get_buckets( $session )) {
  646.             my $total = $self->get_bucket_word_count( $session, $bucket );
  647.  
  648.             if ( $total != 0 ) {
  649.                 $self->{bucket_start__}{$userid}{$bucket} = log( $total /
  650.                                                                  $wc );
  651.             } else {
  652.                 $self->{bucket_start__}{$userid}{$bucket} = 0;
  653.             }
  654.         }
  655.     } else {
  656.         $self->{not_likely__}{$userid} = 0;
  657.     }
  658. }
  659.  
  660. #----------------------------------------------------------------------------
  661. #
  662. # db_connect__
  663. #
  664. # Connects to the POPFile database and returns 1 if successful
  665. #
  666. #----------------------------------------------------------------------------
  667. sub db_connect__
  668. {
  669.     my ( $self ) = @_;
  670.  
  671.     # Connect to the database, note that the database must exist for
  672.     # this to work, to make this easy for people POPFile we will
  673.     # create the database automatically here using the file
  674.     # 'popfile.sql' which should be located in the same directory the
  675.     # Classifier/Bayes.pm module
  676.  
  677.     # If we are using SQLite then the dbname is actually the name of a
  678.     # file, and hence we treat it like one, otherwise we leave it
  679.     # alone
  680.  
  681.     my $dbname;
  682.     my $dbconnect = $self->config_( 'dbconnect' );
  683.     my $dbpresent;
  684.     my $sqlite = ( $dbconnect =~ /sqlite/i );
  685.  
  686.     if ( $sqlite ) {
  687.         $dbname = $self->get_user_path_( $self->config_( 'database' ) );
  688.         $dbpresent = ( -e $dbname ) || 0;                
  689.     } else {
  690.         $dbname = $self->config_( 'database' );
  691.         $dbpresent = 1;
  692.     }
  693.  
  694.     # Record whether we are using SQLite or not and the name of the
  695.     # database so that other routines can access it; this is used by
  696.     # the backup_database__ routine to make a backup copy of the
  697.     # database when using SQLite.
  698.  
  699.     $self->{db_is_sqlite__} = $sqlite;
  700.     $self->{db_name__}      = $dbname;
  701.  
  702.     # Now perform the connect, note that this is database independent
  703.     # at this point, the actual database that we connect to is defined
  704.     # by the dbconnect parameter.
  705.  
  706.     $dbconnect =~ s/\$dbname/$dbname/g;
  707.  
  708.     $self->log_( 0, "Attempting to connect to $dbconnect ($dbpresent)" );
  709.  
  710.     $self->{db__} = DBI->connect( $dbconnect,                    # PROFILE BLOCK START
  711.                                   $self->config_( 'dbuser' ),
  712.                                   $self->config_( 'dbauth' ) );  # PROFILE BLOCK STOP
  713.                                   
  714.     $self->log_( 0, "Using SQLite library version " . $self->{db__}{sqlite_version});
  715.     
  716.     # We check to make sure we're not using DBD::SQLite 1.05 or greater
  717.     # which uses SQLite V 3 If so, we'll use DBD::SQLite2 and SQLite 2.8,
  718.     # which is still compatible with old databases
  719.     
  720.     if ( $self->{db__}{sqlite_version} gt $self->config_('bad_sqlite_version' ) )  {
  721.         $self->log_( 0, "Substituting DBD::SQLite2 for DBD::SQLite 1.05" );        
  722.         $self->log_( 0, "Please install DBD::SQLite2 and set dbconnect to use DBD::SQLite2" );
  723.         
  724.         $dbconnect =~ s/SQLite:/SQLite2:/;
  725.         
  726.         undef $self->{db__};
  727. #         $self->db_disconnect__();
  728.         
  729.         $self->{db__} = DBI->connect( $dbconnect,                    # PROFILE BLOCK START
  730.                                       $self->config_( 'dbuser' ),
  731.                                       $self->config_( 'dbauth' ) );  # PROFILE BLOCK STOP        
  732.     }
  733.  
  734.     if ( !defined( $self->{db__} ) ) {
  735.         $self->log_( 0, "Failed to connect to database and got error $DBI::errstr" );
  736.         return 0;
  737.     }
  738.  
  739.     if ( !$dbpresent ) {
  740.         if ( !$self->insert_schema__( $sqlite ) ) {
  741.             return 0;
  742.         }
  743.     }
  744.  
  745.     # Now check for a need to upgrade the database because the schema
  746.     # has been changed.  From POPFile v0.22.0 there's a special
  747.     # 'popfile' table inside the database that contains the schema
  748.     # version number.  If the version number doesn't match or is
  749.     # missing then do the upgrade.
  750.  
  751.     open SCHEMA, '<' . $self->get_root_path_( 'Classifier/popfile.sql' );
  752.     <SCHEMA> =~ /-- POPFILE SCHEMA (\d+)/;
  753.     my $version = $1;
  754.     close SCHEMA;
  755.  
  756.     my $need_upgrade = 1;
  757.  
  758.     #
  759.     # retrieve the SQL_IDENTIFIER_QUOTE_CHAR for the database then use it
  760.     # to strip off any sqlquotechars from the table names we retrieve
  761.     #
  762.  
  763.     my $sqlquotechar = $self->{db__}->get_info(29) || ''; 
  764.     my @tables = map { s/$sqlquotechar//g; $_ } ($self->{db__}->tables());
  765.  
  766.     foreach my $table (@tables) {
  767.         if ( $table eq 'popfile' ) {
  768.             my @row = $self->{db__}->selectrow_array(
  769.                'select version from popfile;' );
  770.  
  771.             if ( $#row == 0 ) {
  772.                 $need_upgrade = ( $row[0] != $version );
  773.             }
  774.         }
  775.     }
  776.  
  777.     if ( $need_upgrade ) {
  778.  
  779.         print "\n\nDatabase schema is outdated, performing automatic upgrade\n";
  780.         # The database needs upgrading, so we are going to dump out
  781.         # all the data in the database as INSERT statements in a
  782.         # temporary file, then DROP all the tables in the database,
  783.         # then recreate the schema from the new schema and finally
  784.         # rerun the inserts.
  785.  
  786.         my $i = 0;
  787.         my $ins_file = $self->get_user_path_( 'insert.sql' );
  788.         open INSERT, '>' . $ins_file;
  789.  
  790.         foreach my $table (@tables) {
  791.             next if ( $table eq 'popfile' );
  792.             if ( $sqlite && ( $table =~ /^sqlite_/ ) ) {
  793.                 next;
  794.             }
  795.             if ( $i > 99 ) {
  796.                 print "\n";
  797.             }
  798.             print "    Saving table $table\n    ";
  799.  
  800.             my $t = $self->{db__}->prepare( "select * from $table;" );
  801.             $t->execute;
  802.             $i = 0;
  803.             while ( 1 ) {
  804.                 if ( ( ++$i % 100 ) == 0 ) {
  805.                     print "[$i]";
  806.                     flush STDOUT;
  807.                 }
  808.                 my @rows = $t->fetchrow_array;
  809.  
  810.                 last if ( $#rows == -1 );
  811.  
  812.                 print INSERT "INSERT INTO $table (";
  813.                 for my $i (0..$t->{NUM_OF_FIELDS}-1) {
  814.                     if ( $i != 0 ) {
  815.                         print INSERT ',';
  816.                     }
  817.                     print INSERT $t->{NAME}->[$i];
  818.                 }
  819.                 print INSERT ') VALUES (';
  820.                 for my $i (0..$t->{NUM_OF_FIELDS}-1) {
  821.                     if ( $i != 0 ) {
  822.                         print INSERT ',';
  823.                     }
  824.                     my $val = $rows[$i];
  825.                     if ( $t->{TYPE}->[$i] !~ /^int/i ) {
  826.                         $val = '' if ( !defined( $val ) );
  827.                         $val = $self->{db__}->quote( $val );
  828.                     } else {
  829.                         $val = 'NULL' if ( !defined( $val ) );
  830.                     }
  831.                     print INSERT $val;
  832.                 }
  833.                 print INSERT ");\n";
  834.             }
  835.         }
  836.  
  837.         close INSERT;
  838.  
  839.         if ( $i > 99 ) {
  840.             print "\n";
  841.         }
  842.  
  843.         foreach my $table (@tables) {
  844.             if ( $sqlite && ( $table =~ /^sqlite_/ ) ) {
  845.                 next;
  846.             }
  847.             print "    Dropping old table $table\n";
  848.             $self->{db__}->do( "DROP TABLE $table;" );
  849.         }
  850.  
  851.         print "    Inserting new database schema\n";
  852.         if ( !$self->insert_schema__( $sqlite ) ) {
  853.             return 0;
  854.         }
  855.  
  856.         print "    Restoring old data\n    ";
  857.  
  858.         $self->{db__}->begin_work;
  859.         open INSERT, '<' . $ins_file;
  860.         $i = 0;
  861.         while ( <INSERT> ) {
  862.             if ( ( ++$i % 100 ) == 0 ) {
  863.                print "[$i]";
  864.                flush STDOUT;
  865.             }
  866.             s/[\r\n]//g;
  867.             $self->{db__}->do( $_ );
  868.         }
  869.         close INSERT;
  870.         $self->{db__}->commit;
  871.  
  872.         unlink $ins_file;
  873.         print "\nDatabase upgrade complete\n\n";
  874.     }
  875.  
  876.     # Now prepare common SQL statements for use, as a matter of convention the
  877.     # parameters to each statement always appear in the following order:
  878.     #
  879.     # user
  880.     # bucket
  881.     # word
  882.     # parameter
  883.  
  884.     $self->{db_get_buckets__} = $self->{db__}->prepare(                                 # PROFILE BLOCK START
  885.              'select name, id, pseudo from buckets
  886.                   where buckets.userid = ?;' );                                         # PROFILE BLOCK STOP
  887.  
  888.     $self->{db_get_wordid__} = $self->{db__}->prepare(                                  # PROFILE BLOCK START
  889.              'select id from words
  890.                   where words.word = ? limit 1;' );                                     # PROFILE BLOCK STOP
  891.  
  892.     $self->{db_get_userid__} = $self->{db__}->prepare(                                  # PROFILE BLOCK START
  893.              'select id from users where name = ?
  894.                                      and password = ? limit 1;' );                      # PROFILE BLOCK STOP
  895.  
  896.     $self->{db_get_word_count__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  897.              'select matrix.times from matrix
  898.                   where matrix.bucketid = ? and
  899.                         matrix.wordid = ? limit 1;' );                                  # PROFILE BLOCK STOP
  900.  
  901.     $self->{db_put_word_count__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  902.            'replace into matrix ( bucketid, wordid, times ) values ( ?, ?, ? );' );     # PROFILE BLOCK STOP
  903.  
  904.     $self->{db_get_bucket_unique_counts__} = $self->{db__}->prepare(                    # PROFILE BLOCK START
  905.              'select count(matrix.wordid), buckets.name from matrix, buckets
  906.                   where buckets.userid = ?
  907.                     and matrix.bucketid = buckets.id
  908.                   group by buckets.name;' );                                            # PROFILE BLOCK STOP
  909.  
  910.     $self->{db_get_bucket_word_counts__} = $self->{db__}->prepare(                      # PROFILE BLOCK START
  911.              'select sum(matrix.times), buckets.name from matrix, buckets
  912.                   where matrix.bucketid = buckets.id
  913.                     and buckets.userid = ?
  914.                     group by buckets.name;' );                                          # PROFILE BLOCK STOP
  915.  
  916.     $self->{db_get_unique_word_count__} = $self->{db__}->prepare(                       # PROFILE BLOCK START
  917.              'select count(matrix.wordid) from matrix, buckets
  918.                   where matrix.bucketid = buckets.id and
  919.                         buckets.userid = ?;' );                                         # PROFILE BLOCK STOP
  920.  
  921.     $self->{db_get_full_total__} = $self->{db__}->prepare(                              # PROFILE BLOCK START
  922.              'select sum(matrix.times) from matrix, buckets
  923.                   where buckets.userid = ? and
  924.                         matrix.bucketid = buckets.id;' );                               # PROFILE BLOCK STOP
  925.  
  926.     $self->{db_get_bucket_parameter__} = $self->{db__}->prepare(                        # PROFILE BLOCK START
  927.              'select bucket_params.val from bucket_params
  928.                   where bucket_params.bucketid = ? and
  929.                         bucket_params.btid = ?;' );                                     # PROFILE BLOCK STOP
  930.  
  931.     $self->{db_set_bucket_parameter__} = $self->{db__}->prepare(                        # PROFILE BLOCK START
  932.            'replace into bucket_params ( bucketid, btid, val ) values ( ?, ?, ? );' );  # PROFILE BLOCK STOP
  933.  
  934.     $self->{db_get_bucket_parameter_default__} = $self->{db__}->prepare(                # PROFILE BLOCK START
  935.              'select bucket_template.def from bucket_template
  936.                   where bucket_template.id = ?;' );                                     # PROFILE BLOCK STOP
  937.  
  938.     $self->{db_get_buckets_with_magnets__} = $self->{db__}->prepare(                    # PROFILE BLOCK START
  939.              'select buckets.name from buckets, magnets
  940.                   where buckets.userid = ? and
  941.                         magnets.id != 0 and
  942.                         magnets.bucketid = buckets.id group by buckets.name order by buckets.name;' );
  943.                                                                                         # PROFILE BLOCK STOP
  944.     $self->{db_delete_zero_words__} = $self->{db__}->prepare(                           # PROFILE BLOCK START
  945.              'delete from matrix
  946.                   where matrix.times = 0
  947.                     and matrix.bucketid = ?;' );                                        # PROFILE BLOCK STOP
  948.  
  949.     # Get the mapping from parameter names to ids into a local hash
  950.  
  951.     my $h = $self->{db__}->prepare( "select name, id from bucket_template;" );
  952.     $h->execute;
  953.     while ( my $row = $h->fetchrow_arrayref ) {
  954.         $self->{db_parameterid__}{$row->[0]} = $row->[1];
  955.     }
  956.     $h->finish;
  957.  
  958.     return 1;
  959. }
  960.  
  961. #----------------------------------------------------------------------------
  962. #
  963. # insert_schema__
  964. #
  965. # Insert the POPFile schema in a database
  966. #
  967. # $sqlite          Set to 1 if this is a SQLite database
  968. #
  969. #----------------------------------------------------------------------------
  970. sub insert_schema__
  971. {
  972.     my ( $self, $sqlite ) = @_;
  973.  
  974.     if ( -e $self->get_root_path_( 'Classifier/popfile.sql' ) ) {
  975.         my $schema = '';
  976.  
  977.         $self->log_( 0, "Creating database schema" );
  978.  
  979.         open SCHEMA, '<' . $self->get_root_path_( 'Classifier/popfile.sql' );
  980.         while ( <SCHEMA> ) {
  981.             next if ( /^--/ );
  982.             next if ( !/[a-z;]/ );
  983.             s/--.*$//;
  984.  
  985.             # If the line begins 'alter' and we are doing SQLite then ignore
  986.             # the line
  987.  
  988.             if ( $sqlite && ( /^alter/i ) ) {
  989.                 next;
  990.             }
  991.  
  992.             $schema .= $_;
  993.  
  994.             if ( ( /end;/ ) || ( /\);/ ) || ( /^alter/i ) ) {
  995.                 $self->{db__}->do( $schema );
  996.                 $schema = '';
  997.             }
  998.         }
  999.         close SCHEMA;
  1000.         return 1;
  1001.     } else {
  1002.         $self->log_( 0, "Can't find the database schema" );
  1003.         return 0;
  1004.     }
  1005. }
  1006.  
  1007. #----------------------------------------------------------------------------
  1008. #
  1009. # db_disconnect__
  1010. #
  1011. # Disconnect from the POPFile database
  1012. #
  1013. #----------------------------------------------------------------------------
  1014. sub db_disconnect__
  1015. {
  1016.     my ( $self ) = @_;
  1017.  
  1018.     $self->{db_get_buckets__}->finish;
  1019.     $self->{db_get_wordid__}->finish;
  1020.     $self->{db_get_userid__}->finish;
  1021.     $self->{db_get_word_count__}->finish;
  1022.     $self->{db_put_word_count__}->finish;
  1023.     $self->{db_get_bucket_unique_counts__}->finish;
  1024.     $self->{db_get_bucket_word_counts__}->finish;
  1025.     $self->{db_get_unique_word_count__}->finish;
  1026.     $self->{db_get_full_total__}->finish;
  1027.     $self->{db_get_bucket_parameter__}->finish;
  1028.     $self->{db_set_bucket_parameter__}->finish;
  1029.     $self->{db_get_bucket_parameter_default__}->finish;
  1030.     $self->{db_get_buckets_with_magnets__}->finish;
  1031.     $self->{db_delete_zero_words__}->finish;
  1032.  
  1033.     if ( defined( $self->{db__} ) ) {
  1034.         $self->{db__}->disconnect;
  1035.         undef $self->{db__};
  1036.     }
  1037. }
  1038.  
  1039. #----------------------------------------------------------------------------
  1040. #
  1041. # db_update_cache__
  1042. #
  1043. # Updates our local cache of user and bucket ids.
  1044. #
  1045. # $session           Must be a valid session
  1046. #
  1047. #----------------------------------------------------------------------------
  1048. sub db_update_cache__
  1049. {
  1050.     my ( $self, $session ) = @_;
  1051.  
  1052.     my $userid = $self->valid_session_key__( $session );
  1053.     return undef if ( !defined( $userid ) );
  1054.  
  1055.     delete $self->{db_bucketid__}{$userid};
  1056.  
  1057.     $self->{db_get_buckets__}->execute( $userid );
  1058.     while ( my $row = $self->{db_get_buckets__}->fetchrow_arrayref ) {
  1059.         $self->{db_bucketid__}{$userid}{$row->[0]}{id} = $row->[1];
  1060.         $self->{db_bucketid__}{$userid}{$row->[0]}{pseudo} = $row->[2];
  1061.         $self->{db_bucketcount__}{$userid}{$row->[0]} = 0;
  1062.     }
  1063.  
  1064.     $self->{db_get_bucket_word_counts__}->execute( $userid );
  1065.  
  1066.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  1067.         $self->{db_bucketcount__}{$userid}{$b} = 0;
  1068.         $self->{db_bucketunique__}{$userid}{$b} = 0;
  1069.     }
  1070.  
  1071.     while ( my $row = $self->{db_get_bucket_word_counts__}->fetchrow_arrayref ) {
  1072.         $self->{db_bucketcount__}{$userid}{$row->[1]} = $row->[0];
  1073.     }
  1074.  
  1075.     $self->{db_get_bucket_unique_counts__}->execute( $userid );
  1076.  
  1077.     while ( my $row = $self->{db_get_bucket_unique_counts__}->fetchrow_arrayref ) {
  1078.         $self->{db_bucketunique__}{$userid}{$row->[1]} = $row->[0];
  1079.     }
  1080.  
  1081.     $self->update_constants__( $session );
  1082. }
  1083.  
  1084. #----------------------------------------------------------------------------
  1085. #
  1086. # db_get_word_count__
  1087. #
  1088. # Return the 'count' value for a word in a bucket.  If the word is not
  1089. # found in that bucket then returns undef.
  1090. #
  1091. # $session          Valid session ID from get_session_key
  1092. # $bucket           bucket word is in
  1093. # $word             word to lookup
  1094. #
  1095. #----------------------------------------------------------------------------
  1096. sub db_get_word_count__
  1097. {
  1098.     my ( $self, $session, $bucket, $word ) = @_;
  1099.  
  1100.     my $userid = $self->valid_session_key__( $session );
  1101.     return undef if ( !defined( $userid ) );
  1102.  
  1103.     $self->{db_get_wordid__}->execute( $word );
  1104.     my $result = $self->{db_get_wordid__}->fetchrow_arrayref;
  1105.     if ( !defined( $result ) ) {
  1106.         return undef;
  1107.     }
  1108.  
  1109.     my $wordid = $result->[0];
  1110.  
  1111.     $self->{db_get_word_count__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id}, $wordid );
  1112.     $result = $self->{db_get_word_count__}->fetchrow_arrayref;
  1113.     if ( defined( $result ) ) {
  1114.          return $result->[0];
  1115.     } else {
  1116.          return undef;
  1117.     }
  1118. }
  1119.  
  1120. #----------------------------------------------------------------------------
  1121. #
  1122. # db_put_word_count__
  1123. #
  1124. # Update 'count' value for a word in a bucket, if the update fails
  1125. # then returns 0 otherwise is returns 1
  1126. #
  1127. # $session          Valid session ID from get_session_key
  1128. # $bucket           bucket word is in
  1129. # $word             word to update
  1130. # $count            new count value
  1131. #
  1132. #----------------------------------------------------------------------------
  1133. sub db_put_word_count__
  1134. {
  1135.     my ( $self, $session, $bucket, $word, $count ) = @_;
  1136.  
  1137.     my $userid = $self->valid_session_key__( $session );
  1138.     return undef if ( !defined( $userid ) );
  1139.  
  1140.     # We need to have two things before we can start, the id of the
  1141.     # word in the words table (if there's none then we need to add the
  1142.     # word), the bucket id in the buckets table (which must exist)
  1143.  
  1144.     $word = $self->{db__}->quote($word);
  1145.  
  1146.     my $result = $self->{db__}->selectrow_arrayref(
  1147.                      "select words.id from words where words.word = $word limit 1;");
  1148.  
  1149.     if ( !defined( $result ) ) {
  1150.         $self->{db__}->do( "insert into words ( word ) values ( $word );" );
  1151.         $result = $self->{db__}->selectrow_arrayref(
  1152.                      "select words.id from words where words.word = $word limit 1;");
  1153.     }
  1154.  
  1155.     my $wordid = $result->[0];
  1156.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  1157.  
  1158.     $self->{db_put_word_count__}->execute( $bucketid, $wordid, $count );
  1159.  
  1160.     return 1;
  1161. }
  1162.  
  1163. #----------------------------------------------------------------------------
  1164. #
  1165. # upgrade_predatabase_data__
  1166. #
  1167. # Looks for old POPFile data (in flat files or BerkeleyDB tables) and
  1168. # upgrades it to the SQL database.  Data upgraded is removed.
  1169. #
  1170. #----------------------------------------------------------------------------
  1171. sub upgrade_predatabase_data__
  1172. {
  1173.     my ( $self ) = @_;
  1174.     my $c      = 0;
  1175.  
  1176.     # There's an assumption here that this is the single user version
  1177.     # of POPFile and hence what we do is cheat and get a session key
  1178.     # assuming that the user name is admin with password ''
  1179.  
  1180.     my $session = $self->get_session_key( 'admin', '' );
  1181.  
  1182.     if ( !defined( $session ) ) {
  1183.         $self->log_( 0, "Tried to get the session key for user admin and failed; cannot upgrade old data" );
  1184.         return;
  1185.     }
  1186.  
  1187.     my @buckets = glob $self->get_user_path_( $self->config_( 'corpus' ) . '/*' );
  1188.  
  1189.     foreach my $bucket (@buckets) {
  1190.  
  1191.         # A bucket directory must be a directory
  1192.  
  1193.         next unless ( -d $bucket );
  1194.         next unless ( ( -e "$bucket/table" ) || ( -e "$bucket/table.db" ) );
  1195.  
  1196.         return 0 if ( !$self->upgrade_bucket__( $session, $bucket ) );
  1197.  
  1198.         my $color = '';
  1199.  
  1200.         # See if there's a color file specified
  1201.         if ( open COLOR, '<' . "$bucket/color" ) {
  1202.             $color = <COLOR>;
  1203.  
  1204.             # Someone (who shall remain nameless) went in and manually created
  1205.             # empty color files in their corpus directories which would cause
  1206.             # $color at this point to be undefined and hence you'd get warnings
  1207.             # about undefined variables below.  So this little test is to deal
  1208.             # with that user and to make POPFile a little safer which is always
  1209.             # a good thing
  1210.  
  1211.             if ( !defined( $color ) ) {
  1212.                 $color = '';
  1213.             } else {
  1214.                 $color =~ s/[\r\n]//g;
  1215.             }
  1216.             close COLOR;
  1217.             unlink "$bucket/color";
  1218.         }
  1219.  
  1220.         $bucket =~ /([[:alpha:]0-9-_]+)$/;
  1221.         $bucket =  $1;
  1222.  
  1223.         $self->set_bucket_color( $session, $bucket, ($color eq '')?$self->{possible_colors__}[$c]:$color );
  1224.  
  1225.         $c = ($c+1) % ($#{$self->{possible_colors__}}+1);
  1226.     }
  1227.  
  1228.     $self->release_session_key( $session );
  1229.  
  1230.     return 1;
  1231. }
  1232.  
  1233. #----------------------------------------------------------------------------
  1234. #
  1235. # upgrade_bucket__
  1236. #
  1237. # Loads an individual bucket
  1238. #
  1239. # $session           Valid session key from get_session_key
  1240. # $bucket            The bucket name
  1241. #
  1242. #----------------------------------------------------------------------------
  1243. sub upgrade_bucket__
  1244. {
  1245.     my ( $self, $session, $bucket ) = @_;
  1246.  
  1247.     $bucket =~ /([[:alpha:]0-9-_]+)$/;
  1248.     $bucket =  $1;
  1249.  
  1250.     $self->create_bucket( $session, $bucket );
  1251.  
  1252.     if ( open PARAMS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/params" ) ) {
  1253.         while ( <PARAMS> )  {
  1254.             s/[\r\n]//g;
  1255.             if ( /^([[:lower:]]+) ([^\r\n\t ]+)$/ )  {
  1256.                 $self->set_bucket_parameter( $session, $bucket, $1, $2 );
  1257.             }
  1258.         }
  1259.         close PARAMS;
  1260.         unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/params" );
  1261.     }
  1262.  
  1263.     # Pre v0.21.0 POPFile had GLOBAL parameters for subject modification,
  1264.     # XTC and XPL insertion.  To make the upgrade as clean as possible
  1265.     # check these parameters so that if they were OFF we set the equivalent
  1266.     # per bucket to off
  1267.  
  1268.     foreach my $gl ( 'subject', 'xtc', 'xpl' ) {
  1269.         $self->log_( 1, "Checking deprecated parameter GLOBAL_$gl for $bucket\n" );
  1270.         my $val = $self->{configuration__}->deprecated_parameter( "GLOBAL_$gl" );
  1271.         if ( defined( $val ) && ( $val == 0 ) ) {
  1272.             $self->log_( 1, "GLOBAL_$gl is 0 for $bucket, overriding $gl\n" );
  1273.             $self->set_bucket_parameter( $session, $bucket, $gl, 0 );
  1274.         }
  1275.     }
  1276.  
  1277.     # See if there are magnets defined
  1278.     if ( open MAGNETS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/magnets" ) ) {
  1279.         while ( <MAGNETS> )  {
  1280.             s/[\r\n]//g;
  1281.  
  1282.             # Because of a bug in v0.17.9 and earlier of POPFile the text of
  1283.             # some magnets was getting mangled by certain characters having
  1284.             # a \ prepended.  Code here removes the \ in these cases to make
  1285.             # an upgrade smooth.
  1286.  
  1287.             if ( /^([^ ]+) (.+)$/ )  {
  1288.                 my $type  = $1;
  1289.                 my $value = $2;
  1290.  
  1291.                 # Some people were accidently creating magnets with
  1292.                 # trailing whitespace which really confused them later
  1293.                 # when their magnet did not match (see comment in
  1294.                 # UI::HTML::magnet for more detail)
  1295.  
  1296.                 $value =~ s/^[ \t]+//g;
  1297.                 $value =~ s/[ \t]+$//g;
  1298.  
  1299.                 $value =~ s/\\(\?|\*|\||\(|\)|\[|\]|\{|\}|\^|\$|\.)/$1/g;
  1300.                 $self->create_magnet( $session, $bucket, $type, $value );
  1301.             } else {
  1302.  
  1303.                 # This branch is used to catch the original magnets in an
  1304.                 # old version of POPFile that were just there for from
  1305.                 # addresses only
  1306.  
  1307.                 if ( /^(.+)$/ ) {
  1308.                     my $value = $1;
  1309.                     $value =~ s/\\(\?|\*|\||\(|\)|\[|\]|\{|\}|\^|\$|\.)/$1/g;
  1310.                     $self->create_magnet( $session, $bucket, 'from', $value );
  1311.                 }
  1312.             }
  1313.         }
  1314.         close MAGNETS;
  1315.         unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/magnets" );
  1316.     }
  1317.  
  1318.     # If there is no existing table but there is a table file (the old style
  1319.     # flat file used by POPFile for corpus storage) then create the new
  1320.     # database from it thus performing an automatic upgrade.
  1321.  
  1322.     if ( -e $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" ) ) {
  1323.         $self->log_( 0, "Performing automatic upgrade of $bucket corpus from flat file to DBI" );
  1324.  
  1325.         $self->{db__}->begin_work;
  1326.  
  1327.         if ( open WORDS, '<' . $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" ) )  {
  1328.  
  1329.             my $wc = 1;
  1330.  
  1331.             my $first = <WORDS>;
  1332.             if ( defined( $first ) && ( $first =~ s/^__CORPUS__ __VERSION__ (\d+)// ) ) {
  1333.                 if ( $1 != $self->{corpus_version__} )  {
  1334.                     print STDERR "Incompatible corpus version in $bucket\n";
  1335.                     close WORDS;
  1336.                     $self->{db__}->rollback;
  1337.                     return 0;
  1338.                 } else {
  1339.                     $self->log_( 0, "Upgrading bucket $bucket..." );
  1340.  
  1341.                     while ( <WORDS> ) {
  1342.                         if ( $wc % 100 == 0 ) {
  1343.                             $self->log_( 0, "$wc" );
  1344.                         }
  1345.                         $wc += 1;
  1346.                         s/[\r\n]//g;
  1347.  
  1348.                         if ( /^([^\s]+) (\d+)$/ ) {
  1349.                             if ( $2 != 0 ) {
  1350.                                 $self->db_put_word_count__( $session, $bucket, $1, $2 );
  1351.                             }
  1352.                         } else {
  1353.                             $self->log_( 0, "Found entry in corpus for $bucket that looks wrong: \"$_\" (ignoring)" );
  1354.                         }
  1355.                     }
  1356.                 }
  1357.  
  1358.                 if ( $wc > 1 ) {
  1359.                     $wc -= 1;
  1360.                     $self->log_( 0, "(completed $wc words)" );
  1361.                 }
  1362.                 close WORDS;
  1363.             } else {
  1364.                 close WORDS;
  1365.                 $self->{db__}->rollback;
  1366.                 unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" );
  1367.                 return 0;
  1368.             }
  1369.  
  1370.             $self->{db__}->commit;
  1371.             unlink $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table" );
  1372.         }
  1373.     }
  1374.  
  1375.     # Now check to see if there's a BerkeleyDB-style table
  1376.  
  1377.     my $bdb_file = $self->get_user_path_( $self->config_( 'corpus' ) . "/$bucket/table.db" );
  1378.  
  1379.     if ( -e $bdb_file ) {
  1380.         $self->log_( 0, "Performing automatic upgrade of $bucket corpus from BerkeleyDB to DBI" );
  1381.  
  1382.         require BerkeleyDB;
  1383.  
  1384.         my %h;
  1385.         tie %h, "BerkeleyDB::Hash", -Filename => $bdb_file;
  1386.  
  1387.         $self->log_( 0, "Upgrading bucket $bucket..." );
  1388.         $self->{db__}->begin_work;
  1389.  
  1390.         my $wc = 1;
  1391.  
  1392.         for my $word (keys %h) {
  1393.             if ( $wc % 100 == 0 ) {
  1394.                 $self->log_( 0, "$wc" );
  1395.             }
  1396.  
  1397.             next if ( $word =~ /__POPFILE__(LOG__TOTAL|TOTAL|UNIQUE)__/ );
  1398.  
  1399.             $wc += 1;
  1400.             if ( $h{$word} != 0 ) {
  1401.                 $self->db_put_word_count__( $session, $bucket, $word, $h{$word} );
  1402.             }
  1403.         }
  1404.  
  1405.         $wc -= 1;
  1406.         $self->log_( 0, "(completed $wc words)" );
  1407.         $self->{db__}->commit;
  1408.         untie %h;
  1409.         unlink $bdb_file;
  1410.     }
  1411.  
  1412.     return 1;
  1413. }
  1414.  
  1415. #----------------------------------------------------------------------------
  1416. #
  1417. # magnet_match_helper__
  1418. #
  1419. # Helper the determines if a specific string matches a certain magnet
  1420. # type in a bucket, used by magnet_match_
  1421. #
  1422. # $session         Valid session from get_session_key
  1423. # $match           The string to match
  1424. # $bucket          The bucket to check
  1425. # $type            The magnet type to check
  1426. #
  1427. #----------------------------------------------------------------------------
  1428. sub magnet_match_helper__
  1429. {
  1430.     my ( $self, $session, $match, $bucket, $type ) = @_;
  1431.  
  1432.     my $userid = $self->valid_session_key__( $session );
  1433.     return undef if ( !defined( $userid ) );
  1434.  
  1435.     $match = lc($match);
  1436.  
  1437.     # In Japanese and Korean mode, disable locale.  Sorting Japanese
  1438.     # and Korean with "use locale" is memory and time consuming, and
  1439.     # may cause perl crash.
  1440.  
  1441.     my @magnets;
  1442.  
  1443.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  1444.     my $h = $self->{db__}->prepare(                                           # PROFILE BLOCK START
  1445.         "select magnets.val, magnets.id from magnets, users, buckets, magnet_types
  1446.              where buckets.id = $bucketid and
  1447.                    magnets.id != 0 and
  1448.                    users.id = buckets.userid and
  1449.                    magnets.bucketid = buckets.id and
  1450.                    magnet_types.mtype = '$type' and
  1451.                    magnets.mtid = magnet_types.id order by magnets.val;" );   # PROFILE BLOCK STOP
  1452.  
  1453.     $h->execute;
  1454.     while ( my $row = $h->fetchrow_arrayref ) {
  1455.         push @magnets, [$row->[0], $row->[1]];
  1456.     }
  1457.     $h->finish;
  1458.  
  1459.     foreach my $m (@magnets) {
  1460.         my ( $magnet, $id ) = @{$m};
  1461.         $magnet = lc($magnet);
  1462.  
  1463.         for my $i (0..(length($match)-length($magnet))) {
  1464.             if ( substr( $match, $i, length($magnet)) eq $magnet ) {
  1465.                 $self->{magnet_used__}   = 1;
  1466.                 $self->{magnet_detail__} = $id;
  1467.  
  1468.                 return 1;
  1469.             }
  1470.         }
  1471.     }
  1472.  
  1473.     return 0;
  1474. }
  1475.  
  1476. #----------------------------------------------------------------------------
  1477. #
  1478. # magnet_match__
  1479. #
  1480. # Helper the determines if a specific string matches a certain magnet
  1481. # type in a bucket
  1482. #
  1483. # $session         Valid session from get_session_key
  1484. # $match           The string to match
  1485. # $bucket          The bucket to check
  1486. # $type            The magnet type to check
  1487. #
  1488. #----------------------------------------------------------------------------
  1489. sub magnet_match__
  1490. {
  1491.     my ( $self, $session, $match, $bucket, $type ) = @_;
  1492.  
  1493.     return $self->magnet_match_helper__( $session, $match, $bucket, $type );
  1494. }
  1495.  
  1496. #----------------------------------------------------------------------------
  1497. #
  1498. # write_line__
  1499. #
  1500. # Writes a line to a file and parses it unless the classification is
  1501. # already known
  1502. #
  1503. # $file         File handle for file to write line to
  1504. # $line         The line to write
  1505. # $class        (optional) The current classification
  1506. #
  1507. #----------------------------------------------------------------------------
  1508. sub write_line__
  1509. {
  1510.     my ( $self, $file, $line, $class ) = @_;
  1511.  
  1512.     print $file $line if defined( $file );
  1513.  
  1514.     if ( $class eq '' ) {
  1515.         $self->{parser__}->parse_line( $line );
  1516.     }
  1517. }
  1518.  
  1519. #----------------------------------------------------------------------------
  1520. #
  1521. # add_words_to_bucket__
  1522. #
  1523. # Takes words previously parsed by the mail parser and adds/subtracts
  1524. # them to/from a bucket, this is a helper used by
  1525. # add_messages_to_bucket, remove_message_from_bucket
  1526. #
  1527. # $session        Valid session from get_session_key
  1528. # $bucket         Bucket to add to
  1529. # $subtract       Set to -1 means subtract the words, set to 1 means add
  1530. #
  1531. #----------------------------------------------------------------------------
  1532. sub add_words_to_bucket__
  1533. {
  1534.     my ( $self, $session, $bucket, $subtract ) = @_;
  1535.  
  1536.     my $userid = $self->valid_session_key__( $session );
  1537.     return undef if ( !defined( $userid ) );
  1538.  
  1539.     # Map the list of words to a list of counts currently in the database
  1540.     # then update those counts and write them back to the database.
  1541.  
  1542.     my $words;
  1543.     $words = join( ',', map( $self->{db__}->quote( $_ ), (sort keys %{$self->{parser__}{words__}}) ) );
  1544.     $self->{get_wordids__} = $self->{db__}->prepare(        # PROFILE BLOCK START
  1545.              "select id, word
  1546.                   from words
  1547.                   where word in ( $words );" );             # PROFILE BLOCK STOP
  1548.     $self->{get_wordids__}->execute;
  1549.  
  1550.     my @id_list;
  1551.     my %wordmap;
  1552.  
  1553.     while ( my $row = $self->{get_wordids__}->fetchrow_arrayref ) {
  1554.         push @id_list, ($row->[0]);
  1555.         $wordmap{$row->[1]} = $row->[0];
  1556.     }
  1557.  
  1558.     $self->{get_wordids__}->finish;
  1559.  
  1560.     my $ids = join( ',', @id_list );
  1561.  
  1562.     $self->{db_getwords__} = $self->{db__}->prepare(                                         # PROFILE BLOCK START
  1563.              "select matrix.times, matrix.wordid
  1564.                   from matrix
  1565.                   where matrix.wordid in ( $ids )
  1566.                     and matrix.bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};" );  # PROFILE BLOCK STOP
  1567.  
  1568.     $self->{db_getwords__}->execute;
  1569.  
  1570.     my %counts;
  1571.  
  1572.     while ( my $row = $self->{db_getwords__}->fetchrow_arrayref ) {
  1573.         $counts{$row->[1]} = $row->[0];
  1574.     }
  1575.  
  1576.     $self->{db_getwords__}->finish;
  1577.  
  1578.     $self->{db__}->begin_work;
  1579.     foreach my $word (keys %{$self->{parser__}->{words__}}) {
  1580.  
  1581.         # If there's already a count then it means that the word is
  1582.         # already in the database and we have its id in
  1583.         # $wordmap{$word} so for speed we execute the
  1584.         # db_put_word_count__ query here rather than going through
  1585.         # set_value_ which would need to look up the wordid again
  1586.  
  1587.         if ( defined( $wordmap{$word} ) && defined( $counts{$wordmap{$word}} ) ) {
  1588.             $self->{db_put_word_count__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id},               # PROFILE BLOCK START
  1589.                 $wordmap{$word}, $counts{$wordmap{$word}} + $subtract * $self->{parser__}->{words__}{$word} ); # PROFILE BLOCK STOP
  1590.         } else {
  1591.  
  1592.             # If the word is not in the database and we are trying to
  1593.             # subtract then we do nothing because negative values are
  1594.             # meaningless
  1595.  
  1596.             if ( $subtract == 1 ) {
  1597.                 $self->db_put_word_count__( $session, $bucket, $word, $self->{parser__}->{words__}{$word} );
  1598.             }
  1599.         }
  1600.     }
  1601.  
  1602.     # If we were doing a subtract operation it's possible that some of
  1603.     # the words in the bucket now have a zero count and should be
  1604.     # removed
  1605.  
  1606.     if ( $subtract == -1 ) {
  1607.         $self->{db_delete_zero_words__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id} );
  1608.     }
  1609.  
  1610.     $self->{db__}->commit;
  1611. }
  1612.  
  1613. #----------------------------------------------------------------------------
  1614. #
  1615. # echo_to_dot_
  1616. #
  1617. # $mail The stream (created with IO::) to send the message to (the
  1618. # remote mail server)
  1619. # $client (optional) The local mail client (created with IO::) that
  1620. # needs the response
  1621. # $file (optional) A file to print the response to, caller specifies
  1622. # open style
  1623. # $before (optional) String to send to client before the dot is sent
  1624. #
  1625. # echo all information from the $mail server until a single line with
  1626. # a . is seen
  1627. #
  1628. # NOTE Also echoes the line with . to $client but not to $file
  1629. #
  1630. # Returns 1 if there was a . or 0 if reached EOF before we hit the .
  1631. #
  1632. #----------------------------------------------------------------------------
  1633. sub echo_to_dot_
  1634. {
  1635.     my ( $self, $mail, $client, $file, $before ) = @_;
  1636.  
  1637.     my $hit_dot = 0;
  1638.  
  1639.     my $isopen = open FILE, "$file" if ( defined( $file ) );
  1640.     binmode FILE if ($isopen);
  1641.  
  1642.     while ( my $line = $self->slurp_( $mail ) ) {
  1643.  
  1644.         # Check for an abort
  1645.  
  1646.         last if ( $self->{alive_} == 0 );
  1647.  
  1648.         # The termination has to be a single line with exactly a dot
  1649.         # on it and nothing else other than line termination
  1650.         # characters.  This is vital so that we do not mistake a line
  1651.         # beginning with . as the end of the block
  1652.  
  1653.         if ( $line =~ /^\.(\r\n|\r|\n)$/ ) {
  1654.             $hit_dot = 1;
  1655.  
  1656.             if ( defined( $before ) && ( $before ne '' ) ) {
  1657.                 print $client $before if ( defined( $client ) );
  1658.                 print FILE    $before if ( defined( $isopen ) );
  1659.             }
  1660.  
  1661.             # Note that there is no print FILE here.  This is correct
  1662.             # because we do no want the network terminator . to appear
  1663.             # in the file version of any message
  1664.  
  1665.             print $client $line if ( defined( $client ) );
  1666.             last;
  1667.         }
  1668.  
  1669.         print $client $line if ( defined( $client ) );
  1670.         print FILE    $line if ( defined( $isopen ) );
  1671.  
  1672.     }
  1673.  
  1674.     close FILE if ( $isopen );
  1675.  
  1676.     return $hit_dot;
  1677. }
  1678.  
  1679. #----------------------------------------------------------------------------
  1680. #
  1681. # substr_euc__
  1682. #
  1683. # "substr" function which supports EUC Japanese charset
  1684. #
  1685. # $pos      Start position
  1686. # $len      Word length
  1687. #
  1688. #----------------------------------------------------------------------------
  1689. sub substr_euc__
  1690. {
  1691.     my ( $str, $pos, $len ) = @_;
  1692.     my $result_str;
  1693.     my $char;
  1694.     my $count = 0;
  1695.     if ( !$pos ) {
  1696.         $pos = 0;
  1697.     }
  1698.     if ( !$len ) {
  1699.         $len = length( $str );
  1700.     }
  1701.  
  1702.     for ( $pos = 0; $count < $len; $pos++ ) {
  1703.         $char = substr( $str, $pos, 1 );
  1704.         if ( $char =~ /[\x80-\xff]/ ) {
  1705.             $char = substr( $str, $pos++, 2 );
  1706.         }
  1707.         $result_str .= $char;
  1708.         $count++;
  1709.     }
  1710.  
  1711.     return $result_str;
  1712. }
  1713.  
  1714. #----------------------------------------------------------------------------
  1715. #
  1716. # generate_unique_session_key__
  1717. #
  1718. # Returns a unique string based session key that can be used as a key
  1719. # in the api_sessions__
  1720. #
  1721. #----------------------------------------------------------------------------
  1722. sub generate_unique_session_key__
  1723. {
  1724.     my ( $self ) = @_;
  1725.  
  1726.     my @chars = ( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',   # PROFILE BLOCK START
  1727.                   'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'U', 'V', 'W', 'X', 'Y',
  1728.                   'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A' ); # PROFILE BLOCK STOP
  1729.  
  1730.     my $session;
  1731.  
  1732.     do {
  1733.         $session = '';
  1734.         my $length = int( 16 + rand(4) );
  1735.  
  1736.         for my $i (0 .. $length) {
  1737.             my $random = $chars[int( rand(36) )];
  1738.  
  1739.             # Just to add spice to things we sometimes lowercase the value
  1740.  
  1741.             if ( rand(1) < rand(1) ) {
  1742.                 $random = lc($random);
  1743.             }
  1744.  
  1745.             $session .= $random;
  1746.         }
  1747.     } while ( defined( $self->{api_sessions__}{$session} ) );
  1748.  
  1749.     return $session;
  1750. }
  1751.  
  1752. #----------------------------------------------------------------------------
  1753. #
  1754. # release_session_key_private__
  1755. #
  1756. # $session        A session key previously returned by get_session_key
  1757. #
  1758. # Releases and invalidates the session key. Worker function that does the work
  1759. # of release_session_key. 
  1760. #                   ****DO NOT CALL DIRECTLY****
  1761. # unless you want your session key released immediately, possibly preventing
  1762. # asynchronous tasks from completing
  1763. #
  1764. #----------------------------------------------------------------------------
  1765. sub release_session_key_private__
  1766. {
  1767.     my ( $self, $session ) = @_;
  1768.     
  1769.     if ( defined( $self->{api_sessions__}{$session} ) ) {
  1770.         $self->log_( 1, "release_session_key releasing key $session for user $self->{api_sessions__}{$session}" );
  1771.         delete $self->{api_sessions__}{$session};
  1772.     }
  1773. }
  1774.  
  1775. #----------------------------------------------------------------------------
  1776. #
  1777. # valid_session_key__
  1778. #
  1779. # $session                Session key returned by call to get_session_key
  1780. #
  1781. # Returns undef is the session key is not valid, or returns the user
  1782. # ID associated with the session key which can be used in database
  1783. # accesses
  1784. #
  1785. #----------------------------------------------------------------------------
  1786. sub valid_session_key__
  1787. {
  1788.     my ( $self, $session ) = @_;
  1789.  
  1790.     # This provides protection against someone using the XML-RPC
  1791.     # interface and calling this API directly to fish for session
  1792.     # keys, this must be called from within this module
  1793.  
  1794.     return undef if ( caller ne 'Classifier::Bayes' );
  1795.  
  1796.     # If the session key is invalid then wait 1 second.  This is done
  1797.     # to prevent people from calling a POPFile API such as
  1798.     # get_bucket_count with random session keys fishing for a valid
  1799.     # key.  The XML-RPC API is single threaded and hence this will
  1800.     # delay all use of that API by one second.  Of course in normal
  1801.     # use when the user knows the username/password or session key
  1802.     # then there is no delay
  1803.  
  1804.     if ( !defined( $self->{api_sessions__}{$session} ) ) {
  1805.         my ( $package, $filename, $line, $subroutine ) = caller;
  1806.         $self->log_( 0, "Invalid session key $session provided in $package @ $line" );
  1807.         select( undef, undef, undef, 1 );
  1808.     }
  1809.  
  1810.     return $self->{api_sessions__}{$session};
  1811. }
  1812.  
  1813. #----------------------------------------------------------------------------
  1814. #----------------------------------------------------------------------------
  1815. # _____   _____   _____  _______ _____        _______   _______  _____  _____
  1816. #|_____] |     | |_____] |______   |   |      |______   |_____| |_____]   |
  1817. #|       |_____| |       |       __|__ |_____ |______   |     | |       __|__
  1818. #
  1819. # The method below are public and may be accessed by other modules.
  1820. # All of them may be accessed remotely through the XMLRPC.pm module
  1821. # using the XML-RPC protocol
  1822. #
  1823. # Note that every API function expects to be passed a $session which
  1824. # is obtained by first calling get_session_key with a valid username
  1825. # and password.  Once done call the method release_session_key.
  1826. #
  1827. # See POPFile::API for more details
  1828. #
  1829. #----------------------------------------------------------------------------
  1830. #----------------------------------------------------------------------------
  1831.  
  1832. #----------------------------------------------------------------------------
  1833. #
  1834. # get_session_key
  1835. #
  1836. # $user           The name of an existing user
  1837. # $pwd            The user's password
  1838. #
  1839. # Returns a string based session key if the username and password
  1840. # match, or undef if not
  1841. #
  1842. #----------------------------------------------------------------------------
  1843. sub get_session_key
  1844. {
  1845.     my ( $self, $user, $pwd ) = @_;
  1846.  
  1847.     # The password is stored in the database as an MD5 hash of the
  1848.     # username and password concatenated and separated by the string
  1849.     # __popfile__, so compute the hash here
  1850.  
  1851.     my $hash = md5_hex( $user . '__popfile__' . $pwd );
  1852.  
  1853.     $self->{db_get_userid__}->execute( $user, $hash );
  1854.     my $result = $self->{db_get_userid__}->fetchrow_arrayref;
  1855.     if ( !defined( $result ) ) {
  1856.  
  1857.         # The delay of one second here is to prevent people from trying out
  1858.         # username/password combinations at high speed to determine the
  1859.         # credentials of a valid user
  1860.  
  1861.         $self->log_( 0, "Attempt to login with incorrect credentials for user $user" );
  1862.         select( undef, undef, undef, 1 );
  1863.         return undef;
  1864.     }
  1865.  
  1866.     my $session = $self->generate_unique_session_key__();
  1867.  
  1868.     $self->{api_sessions__}{$session} = $result->[0];
  1869.  
  1870.     $self->db_update_cache__( $session );
  1871.  
  1872.     $self->log_( 1, "get_session_key returning key $session for user $self->{api_sessions__}{$session}" );
  1873.  
  1874.     return $session;
  1875. }
  1876.  
  1877. #----------------------------------------------------------------------------
  1878. #
  1879. # release_session_key
  1880. #
  1881. # $session        A session key previously returned by get_session_key
  1882. #
  1883. # Releases and invalidates the session key
  1884. #
  1885. #----------------------------------------------------------------------------
  1886. sub release_session_key
  1887. {
  1888.     my ( $self, $session ) = @_;
  1889.     
  1890.     $self->mq_post_( "RELSE", $session );
  1891. }
  1892.  
  1893.  
  1894. #----------------------------------------------------------------------------
  1895. #
  1896. # get_top_bucket__
  1897. #
  1898. # Helper function used by classify to get the bucket with the highest
  1899. # score from data stored in a matrix of information (see definition of
  1900. # %matrix in classify for details) and a list of potential buckets
  1901. #
  1902. # $userid         User ID for database access
  1903. # $id             ID of a word in $matrix
  1904. # $matrix         Reference to the %matrix hash in classify
  1905. # $buckets        Reference to a list of buckets
  1906. #
  1907. # Returns the bucket in $buckets with the highest score
  1908. #
  1909. #----------------------------------------------------------------------------
  1910. sub get_top_bucket__
  1911. {
  1912.     my ( $self, $userid, $id, $matrix, $buckets ) = @_;
  1913.  
  1914.     my $best_probability = 0;
  1915.     my $top_bucket       = 'unclassified';
  1916.  
  1917.     for my $bucket (@$buckets) {
  1918.         my $probability = 0;
  1919.         if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  1920.             $probability = $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket};
  1921.         }
  1922.  
  1923.         if ( $probability > $best_probability ) {
  1924.             $best_probability = $probability;
  1925.             $top_bucket       = $bucket;
  1926.         }
  1927.     }
  1928.  
  1929.     return $top_bucket;
  1930. }
  1931.  
  1932. #----------------------------------------------------------------------------
  1933. #
  1934. # classify
  1935. #
  1936. # $session   A valid session key returned by a call to get_session_key
  1937. # $file The name of the file containing the text to classify (or undef
  1938. # to use the data already in the parser)
  1939. # $templ     Reference to the UI template used for word score display
  1940. # $matrix (optional) Reference to a hash that will be filled with the
  1941. # word matrix used in classification
  1942. # $idmap (optional) Reference to a hash that will map word ids in the
  1943. # $matrix to actual words
  1944. #
  1945. # Splits the mail message into valid words, then runs the Bayes
  1946. # algorithm to figure out which bucket it belongs in.  Returns the
  1947. # bucket name
  1948. #
  1949. #----------------------------------------------------------------------------
  1950. sub classify
  1951. {
  1952.     my ( $self, $session, $file, $templ, $matrix, $idmap ) = @_;
  1953.     my $msg_total = 0;
  1954.  
  1955.     my $userid = $self->valid_session_key__( $session );
  1956.     return undef if ( !defined( $userid ) );
  1957.  
  1958.     $self->{unclassified__} = log( $self->config_( 'unclassified_weight' ) );
  1959.  
  1960.     $self->{magnet_used__}   = 0;
  1961.     $self->{magnet_detail__} = 0;
  1962.  
  1963.     if ( defined( $file ) ) {
  1964.         $self->{parser__}->parse_file( $file,                                           # PROFILE BLOCK START
  1965.                                        $self->global_config_( 'message_cutoff'   ) );   # PROFILE BLOCK STOP
  1966.     }
  1967.  
  1968.     # Check to see if this email should be classified based on a magnet
  1969.     # Get the list of buckets
  1970.  
  1971.     my @buckets = $self->get_buckets( $session );
  1972.  
  1973.     for my $bucket ($self->get_buckets_with_magnets( $session ))  {
  1974.         for my $type ($self->get_magnet_types_in_bucket( $session, $bucket )) {
  1975.             if ( $self->magnet_match__( $session, $self->{parser__}->get_header($type), $bucket, $type ) ) {
  1976.                 return $bucket;
  1977.             }
  1978.         }
  1979.     }
  1980.  
  1981.     # If the user has not defined any buckets then we escape here
  1982.     # return unclassified
  1983.  
  1984.     return "unclassified" if ( $#buckets == -1 );
  1985.  
  1986.     # The score hash will contain the likelihood that the given
  1987.     # message is in each bucket, the buckets are the keys for score
  1988.  
  1989.     # Set up the initial score as P(bucket)
  1990.  
  1991.     my %score;
  1992.     my %matchcount;
  1993.  
  1994.     # Build up a list of the buckets that are OK to use for
  1995.     # classification (i.e.  that have at least one word in them).
  1996.  
  1997.     my @ok_buckets;
  1998.  
  1999.     for my $bucket (@buckets) {
  2000.         if ( $self->{bucket_start__}{$userid}{$bucket} != 0 ) {
  2001.             $score{$bucket} = $self->{bucket_start__}{$userid}{$bucket};
  2002.             $matchcount{$bucket} = 0;
  2003.             push @ok_buckets, ( $bucket );
  2004.         }
  2005.     }
  2006.  
  2007.     @buckets = @ok_buckets;
  2008.  
  2009.     # For each word go through the buckets and calculate
  2010.     # P(word|bucket) and then calculate P(word|bucket) ^ word count
  2011.     # and multiply to the score
  2012.  
  2013.     my $word_count = 0;
  2014.  
  2015.     # The correction value is used to generate score displays variable
  2016.     # which are consistent with the word scores shown by the GUI's
  2017.     # word lookup feature.  It is computed to make the contribution of
  2018.     # a word which is unrepresented in a bucket zero.  This correction
  2019.     # affects only the values displayed in the display; it has no
  2020.     # effect on the classification process.
  2021.  
  2022.     my $correction = 0;
  2023.  
  2024.     # Classification against the database works in a sequence of steps
  2025.     # to get the fastest time possible.  The steps are as follows:
  2026.     #
  2027.     # 1. Convert the list of words returned by the parser into a list
  2028.     #    of unique word ids that can be used in the database.  This
  2029.     #    requires a select against the database to get the word ids
  2030.     #    (and associated words) which is then converted into two
  2031.     #    things: @id_list which is just the sorted list of word ids
  2032.     #    and %idmap which maps a word to its id.
  2033.     #
  2034.     # 2. Then run a second select that get the triplet (count, id,
  2035.     #    bucket) for each word id and each bucket.  The triplet
  2036.     #    contains the word count from the database for each bucket and
  2037.     #    each id, where there is an entry. That data gets loaded into
  2038.     #    the sparse matrix %matrix.
  2039.     #
  2040.     # 3. Do the normal classification loop as before running against
  2041.     # the @id_list for the words and for each bucket.  If there's an
  2042.     # entry in %matrix for the id/bucket combination then calculate
  2043.     # the probability, otherwise use the not_likely probability.
  2044.     #
  2045.     # NOTE.  Since there is a single not_likely probability we do not
  2046.     # worry about the fact that the select in 1 might return a shorter
  2047.     # list of words than was found in the message (because some words
  2048.     # are not in the database) since the missing words will be the
  2049.     # same for all buckets and hence constitute a fixed scaling factor
  2050.     # on all the buckets which is irrelevant in deciding which the
  2051.     # winning bucket is.
  2052.  
  2053.     my $words;
  2054.     $words = join( ',', map( $self->{db__}->quote( $_ ), (sort keys %{$self->{parser__}{words__}}) ) );
  2055.     $self->{get_wordids__} = $self->{db__}->prepare(  # PROFILE BLOCK START
  2056.              "select id, word
  2057.                   from words
  2058.                   where word in ( $words )
  2059.                   order by id;" );                    # PROFILE BLOCK STOP
  2060.     $self->{get_wordids__}->execute;
  2061.  
  2062.     my @id_list;
  2063.     my %temp_idmap;
  2064.  
  2065.     if ( !defined( $idmap ) ) {
  2066.         $idmap = \%temp_idmap;
  2067.     }
  2068.  
  2069.     while ( my $row = $self->{get_wordids__}->fetchrow_arrayref ) {
  2070.         push @id_list, ($row->[0]);
  2071.         $$idmap{$row->[0]} = $row->[1];
  2072.     }
  2073.  
  2074.     $self->{get_wordids__}->finish;
  2075.  
  2076.     my $ids = join( ',', @id_list );
  2077.  
  2078.     $self->{db_classify__} = $self->{db__}->prepare(            # PROFILE BLOCK START
  2079.              "select matrix.times, matrix.wordid, buckets.name
  2080.                   from matrix, buckets
  2081.                   where matrix.wordid in ( $ids )
  2082.                     and matrix.bucketid = buckets.id
  2083.                     and buckets.userid = $userid;" );           # PROFILE BLOCK STOP
  2084.  
  2085.     $self->{db_classify__}->execute;
  2086.  
  2087.     # %matrix maps wordids and bucket names to counts
  2088.     # $matrix{$wordid}{$bucket} == $count
  2089.  
  2090.     my %temp_matrix;
  2091.  
  2092.     if ( !defined( $matrix ) ) {
  2093.         $matrix = \%temp_matrix;
  2094.     }
  2095.  
  2096.     while ( my $row = $self->{db_classify__}->fetchrow_arrayref ) {
  2097.         $$matrix{$row->[1]}{$row->[2]} = $row->[0];
  2098.     }
  2099.  
  2100.     $self->{db_classify__}->finish;
  2101.  
  2102.     foreach my $id (@id_list) {
  2103.         $word_count += 2;
  2104.         my $wmax = -10000;
  2105.  
  2106.         foreach my $bucket (@buckets) {
  2107.             my $probability = 0;
  2108.  
  2109.             if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  2110.                 $probability = log( $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket} );
  2111.             }
  2112.  
  2113.             $matchcount{$bucket} += $self->{parser__}{words__}{$$idmap{$id}} if ($probability != 0);
  2114.             $probability = $self->{not_likely__}{$userid} if ( $probability == 0 );
  2115.             $wmax = $probability if ( $wmax < $probability );
  2116.             $score{$bucket} += ( $probability * $self->{parser__}{words__}{$$idmap{$id}} );
  2117.         }
  2118.  
  2119.         if ($wmax > $self->{not_likely__}{$userid}) {
  2120.             $correction += $self->{not_likely__}{$userid} * $self->{parser__}{words__}{$$idmap{$id}};
  2121.         } else {
  2122.             $correction += $wmax * $self->{parser__}{words__}{$$idmap{$id}};
  2123.         }
  2124.     }
  2125.  
  2126.     # Now sort the scores to find the highest and return that bucket
  2127.     # as the classification
  2128.  
  2129.     my @ranking = sort {$score{$b} <=> $score{$a}} keys %score;
  2130.  
  2131.     my %raw_score;
  2132.     my $base_score = $score{$ranking[0]};
  2133.     my $total = 0;
  2134.  
  2135.     # If the first and second bucket are too close in their
  2136.     # probabilities, call the message unclassified.  Also if there are
  2137.     # fewer than 2 buckets.
  2138.  
  2139.     my $class = 'unclassified';
  2140.  
  2141.     if ( @buckets > 1 && $score{$ranking[0]} > ( $score{$ranking[1]} + $self->{unclassified__} ) ) {
  2142.         $class = $ranking[0];
  2143.     }
  2144.  
  2145.     # Compute the total of all the scores to generate the normalized
  2146.     # scores and probability estimate.  $total is always 1 after the
  2147.     # first loop iteration, so any additional term less than 2 ** -54
  2148.     # is insignificant, and need not be computed.
  2149.  
  2150.     my $ln2p_54 = -54 * log(2);
  2151.  
  2152.     foreach my $b (@ranking) {
  2153.         $raw_score{$b} = $score{$b};
  2154.         $score{$b} -= $base_score;
  2155.  
  2156.         $total += exp($score{$b}) if ($score{$b} > $ln2p_54 );
  2157.     }
  2158.  
  2159.     if ($self->{wordscores__} && defined($templ) ) {
  2160.         my %qm = %{$self->{parser__}->quickmagnets()};
  2161.         my $mlen = scalar(keys %{$self->{parser__}->quickmagnets()});
  2162.  
  2163.         if ( $mlen >= 0 ) {
  2164.             $templ->param( 'View_QuickMagnets_If' => 1 );
  2165.             $templ->param( 'View_QuickMagnets_Count' => ($mlen + 1) );
  2166.             my @buckets = $self->get_buckets( $session );
  2167.             my $i = 0;
  2168.             my %types = $self->get_magnet_types( $session );
  2169.  
  2170.             my @bucket_data;
  2171.             foreach my $bucket (@buckets) {
  2172.                 my %row_data;
  2173.                 $row_data{View_QuickMagnets_Bucket} = $bucket;
  2174.                 $row_data{View_QuickMagnets_Bucket_Color} = $self->get_bucket_color( $session, $bucket );
  2175.                 push ( @bucket_data, \%row_data );
  2176.             }
  2177.  
  2178.             my @qm_data;
  2179.             foreach my $type (sort keys %types) {
  2180.                 my %row_data;
  2181.  
  2182.                 if (defined $qm{$type}) {
  2183.                     $i++;
  2184.  
  2185.                     $row_data{View_QuickMagnets_Type} = $type;
  2186.                     $row_data{View_QuickMagnets_I} = $i;
  2187.                     $row_data{View_QuickMagnets_Loop_Buckets} = \@bucket_data;
  2188.  
  2189.                     my @magnet_data;
  2190.                     foreach my $magnet ( @{$qm{$type}} ) {
  2191.                         my %row_magnet;
  2192.                         $row_magnet{View_QuickMagnets_Magnet} = $magnet;
  2193.                         push ( @magnet_data, \%row_magnet );
  2194.                     }
  2195.                     $row_data{View_QuickMagnets_Loop_Magnets} = \@magnet_data;
  2196.  
  2197.                     push ( @qm_data, \%row_data );
  2198.                 }
  2199.             }
  2200.             $templ->param( 'View_QuickMagnets_Loop' => \@qm_data );
  2201.         }
  2202.  
  2203.         $templ->param( 'View_Score_If_Score' => $self->{wmformat__} eq 'score' );
  2204.         my $log10 = log(10.0);
  2205.  
  2206.         my @score_data;
  2207.         foreach my $b (@ranking) {
  2208.              my %row_data;
  2209.              my $prob = exp($score{$b})/$total;
  2210.              my $probstr;
  2211.              my $rawstr;
  2212.  
  2213.              # If the computed probability would display as 1, display
  2214.              # it as .999999 instead.  We don't want to give the
  2215.              # impression that POPFile is ever completely sure of its
  2216.              # classification.
  2217.  
  2218.              if ($prob >= .999999) {
  2219.                  $probstr = sprintf("%12.6f", 0.999999);
  2220.              } else {
  2221.                  if ($prob >= 0.1 || $prob == 0.0) {
  2222.                      $probstr = sprintf("%12.6f", $prob);
  2223.                  } else {
  2224.                     $probstr = sprintf("%17.6e", $prob);
  2225.                  }
  2226.              }
  2227.  
  2228.              my $color = $self->get_bucket_color( $session, $b );
  2229.  
  2230.              $row_data{View_Score_Bucket} = $b;
  2231.              $row_data{View_Score_Bucket_Color} = $color;
  2232.              $row_data{View_Score_MatchCount} = $matchcount{$b};
  2233.              $row_data{View_Score_ProbStr} = $probstr;
  2234.  
  2235.              if ($self->{wmformat__} eq 'score') {
  2236.                  $row_data{View_Score_If_Score} = 1;
  2237.                  $rawstr = sprintf("%12.6f", ($raw_score{$b} - $correction)/$log10);
  2238.                  $row_data{View_Score_RawStr} = $rawstr;
  2239.              }
  2240.              push ( @score_data, \%row_data );
  2241.         }
  2242.         $templ->param( 'View_Score_Loop_Scores' => \@score_data );
  2243.  
  2244.         if ( $self->{wmformat__} ne '' ) {
  2245.             $templ->param( 'View_Score_If_Table' => 1 );
  2246.  
  2247.             my @header_data;
  2248.             foreach my $ix (0..($#buckets > 7? 7: $#buckets)) {
  2249.                 my %row_data;
  2250.                 my $bucket = $ranking[$ix];
  2251.                 my $bucketcolor  = $self->get_bucket_color( $session, $bucket );
  2252.                 $row_data{View_Score_Bucket} = $bucket;
  2253.                 $row_data{View_Score_Bucket_Color} = $bucketcolor;
  2254.                 push ( @header_data, \%row_data );
  2255.             }
  2256.             $templ->param( 'View_Score_Loop_Bucket_Header' => \@header_data );
  2257.  
  2258.             my %wordprobs;
  2259.  
  2260.             # If the word matrix is supposed to show probabilities,
  2261.             # compute them, saving the results in %wordprobs.
  2262.  
  2263.             if ( $self->{wmformat__} eq 'prob') {
  2264.                 foreach my $id (@id_list) {
  2265.                     my $sumfreq = 0;
  2266.                     my %wval;
  2267.                     foreach my $bucket (@ranking) {
  2268.                         $wval{$bucket} = $$matrix{$id}{$bucket} || 0;
  2269.                         $sumfreq += $wval{$bucket};
  2270.                     }
  2271.  
  2272.                     # If $sumfreq is still zero then this word didn't
  2273.                     # appear in any buckets so we shouldn't create
  2274.                     # wordprobs entries for it
  2275.  
  2276.                     if ( $sumfreq != 0 ) {
  2277.                         foreach my $bucket (@ranking) {
  2278.                             $wordprobs{$bucket,$id} = $wval{$bucket} / $sumfreq;
  2279.                         }
  2280.                     }
  2281.                 }
  2282.             }
  2283.  
  2284.             my @ranked_ids;
  2285.             if ($self->{wmformat__} eq 'prob') {
  2286.                 @ranked_ids = sort {($wordprobs{$ranking[0],$b}||0) <=> ($wordprobs{$ranking[0],$a}||0)} @id_list;
  2287.             } else {
  2288.                 @ranked_ids = sort {($$matrix{$b}{$ranking[0]}||0) <=> ($$matrix{$a}{$ranking[0]}||0)} @id_list;
  2289.             }
  2290.  
  2291.             my @word_data;
  2292.             my %chart;
  2293.             foreach my $id (@ranked_ids) {
  2294.                 my %row_data;
  2295.                 my $known = 0;
  2296.  
  2297.                 foreach my $bucket (@ranking) {
  2298.                     if ( defined( $$matrix{$id}{$bucket} ) ) {
  2299.                         $known = 1;
  2300.                         last;
  2301.                     }
  2302.                 }
  2303.  
  2304.                 if ( $known == 1 ) {
  2305.                     my $wordcolor = $self->get_bucket_color( $session, $self->get_top_bucket__( $userid, $id, $matrix, \@ranking ) );
  2306.                     my $count = $self->{parser__}->{words__}{$$idmap{$id}};
  2307.  
  2308.                     $row_data{View_Score_Word} = $$idmap{$id};
  2309.                     $row_data{View_Score_Word_Color} = $wordcolor;
  2310.                     $row_data{View_Score_Word_Count} = $count;
  2311.  
  2312.                     my $base_probability = 0;
  2313.                     if ( defined($$matrix{$id}{$ranking[0]}) && ( $$matrix{$id}{$ranking[0]} > 0 ) ) {
  2314.                         $base_probability = log( $$matrix{$id}{$ranking[0]} / $self->{db_bucketcount__}{$userid}{$ranking[0]} );
  2315.                     }
  2316.  
  2317.                     my @per_bucket;
  2318.                     my @score;
  2319.                     foreach my $ix (0..($#buckets > 7? 7: $#buckets)) {
  2320.                         my %bucket_row;
  2321.                         my $bucket = $ranking[$ix];
  2322.                         my $probability = 0;
  2323.                         if ( defined($$matrix{$id}{$bucket}) && ( $$matrix{$id}{$bucket} > 0 ) ) {
  2324.                             $probability = log( $$matrix{$id}{$bucket} / $self->{db_bucketcount__}{$userid}{$bucket} );
  2325.                         }
  2326.                         my $color = 'black';
  2327.  
  2328.                         if ( $probability >= $base_probability || $base_probability == 0 ) {
  2329.                             $color = $self->get_bucket_color( $session, $bucket );
  2330.                         }
  2331.  
  2332.                         $bucket_row{View_Score_If_Probability} = ( $probability != 0 );
  2333.                         $bucket_row{View_Score_Word_Color} = $color;
  2334.                         if ( $probability != 0 ) {
  2335.                             my $wordprobstr;
  2336.                             if ($self->{wmformat__} eq 'score') {
  2337.                                 $wordprobstr  = sprintf("%12.4f", ($probability - $self->{not_likely__}{$userid})/$log10 );
  2338.                                 push ( @score, $wordprobstr );
  2339.                             } else {
  2340.                                 if ($self->{wmformat__} eq 'prob') {
  2341.                                     $wordprobstr  = sprintf("%12.4f", $wordprobs{$bucket,$id});
  2342.                                 } else {
  2343.                                     $wordprobstr  = sprintf("%13.5f", exp($probability) );
  2344.                                 }
  2345.                             }
  2346.                             $bucket_row{View_Score_Probability} = $wordprobstr;
  2347.                         }
  2348.                         else {
  2349.                             # Scores eq 0 must also be remembered.
  2350.                             push @score, 0;
  2351.                         }
  2352.                         push ( @per_bucket, \%bucket_row );
  2353.                     }
  2354.                     $row_data{View_Score_Loop_Per_Bucket} = \@per_bucket;
  2355.  
  2356.                     # If we are doing the word scores then we build up
  2357.                     # a hash that maps the name of a word to a value
  2358.                     # which is the difference between the word scores
  2359.                     # for the top two buckets.  We later use this to
  2360.                     # draw a chart
  2361.  
  2362.                     if ( $self->{wmformat__} eq 'score' ) {
  2363.                         $chart{$$idmap{$id}} = ( $score[0] || 0 ) - ( $score[1] || 0 );
  2364.                     }
  2365.  
  2366.                     push ( @word_data, \%row_data );
  2367.                 }
  2368.             }
  2369.             $templ->param( 'View_Score_Loop_Words' => \@word_data );
  2370.  
  2371.             if ( $self->{wmformat__} eq 'score' ) {
  2372.                 # Draw a chart that shows how the decision between the top
  2373.                 # two buckets was made.
  2374.  
  2375.                 my @words = sort { $chart{$b} <=> $chart{$a} } keys %chart;
  2376.  
  2377.                 my @chart_data;
  2378.                 my $max_chart = $chart{$words[0]};
  2379.                 my $min_chart = $chart{$words[$#words]};
  2380.                 my $scale = ( $max_chart > $min_chart ) ? 400 / ( $max_chart - $min_chart ) : 0;
  2381.  
  2382.                 my $color_1 = $self->get_bucket_color( $session, $ranking[0] );
  2383.                 my $color_2 = $self->get_bucket_color( $session, $ranking[1] );
  2384.  
  2385.                 $templ->param( 'Bucket_1' => $ranking[0] );
  2386.                 $templ->param( 'Bucket_2' => $ranking[1] );
  2387.  
  2388.                 $templ->param( 'Color_Bucket_1' => $color_1 );
  2389.                 $templ->param( 'Color_Bucket_2' => $color_2 );
  2390.  
  2391.                 $templ->param( 'Score_Bucket_1' => sprintf("%.3f", ($raw_score{$ranking[0]} - $correction)/$log10) );
  2392.                 $templ->param( 'Score_Bucket_2' => sprintf("%.3f", ($raw_score{$ranking[1]} - $correction)/$log10) );
  2393.  
  2394.                 for ( my $i=0; $i <= $#words; $i++ ) {
  2395.                     my $word_1 = $words[$i];
  2396.                     my $word_2 = $words[$#words - $i];
  2397.  
  2398.                     my $width_1 = int( $chart{$word_1} * $scale + .5 );
  2399.                     my $width_2 = int( $chart{$word_2} * $scale - .5 ) * -1;
  2400.  
  2401.                     last if ( $width_1 <=0 && $width_2 <= 0 );
  2402.                     
  2403.                     my %row_data;
  2404.  
  2405.                     $row_data{View_Chart_Word_1} = $word_1;
  2406.                     if ( $width_1 > 0 ) {
  2407.                         $row_data{View_If_Bar_1} = 1;
  2408.                         $row_data{View_Width_1}  = $width_1;
  2409.                         $row_data{View_Color_1}  = $color_1;
  2410.                         $row_data{Score_Word_1}  = sprintf "%.3f", $chart{$word_1};
  2411.                     }
  2412.                     else {
  2413.                         $row_data{View_If_Bar_1} = 0;
  2414.                     }
  2415.  
  2416.                     $row_data{View_Chart_Word_2} = $word_2;
  2417.                     if ( $width_2 > 0 ) {
  2418.                         $row_data{View_If_Bar_2} = 1;
  2419.                         $row_data{View_Width_2}  = $width_2;
  2420.                         $row_data{View_Color_2}  = $color_2;
  2421.                         $row_data{Score_Word_2}  = sprintf "%.3f", $chart{$word_2};
  2422.                     }
  2423.                     else {
  2424.                         $row_data{View_If_Bar_2} = 0;
  2425.                     }
  2426.  
  2427.                     push ( @chart_data, \%row_data );
  2428.                 }
  2429.                 $templ->param( 'View_Loop_Chart' => \@chart_data );
  2430.                 $templ->param( 'If_chart' => 1 );
  2431.             }
  2432.             else {
  2433.                 $templ->param( 'If_chart' => 0 );
  2434.             }
  2435.         }
  2436.     }
  2437.  
  2438.     return $class;
  2439. }
  2440.  
  2441. #----------------------------------------------------------------------------
  2442. #
  2443. # classify_and_modify
  2444. #
  2445. # This method reads an email terminated by . on a line by itself (or
  2446. # the end of stream) from a handle and creates an entry in the
  2447. # history, outputting the same email on another handle with the
  2448. # appropriate header modifications and insertions
  2449. #
  2450. # $session  - A valid session key returned by a call to get_session_key
  2451. # $mail     - an open stream to read the email from
  2452. # $client   - an open stream to write the modified email to
  2453. # $nosave   - set to 1 indicates that this should not save to history
  2454. # $class    - if we already know the classification
  2455. # $slot     - Must be defined if $class is set
  2456. # $echo     - 1 to echo to the client, 0 to supress, defaults to 1
  2457. # $crlf     - The sequence to use at the end of a line in the output,
  2458. #   normally this is left undefined and this method uses $eol (the
  2459. #   normal network end of line), but if this method is being used with
  2460. #   real files you may wish to pass in \n instead
  2461. #
  2462. # Returns a classification if it worked and the slot ID of the history
  2463. # item related to this classification
  2464. #
  2465. # IMPORTANT NOTE: $mail and $client should be binmode
  2466. #
  2467. #----------------------------------------------------------------------------
  2468. sub classify_and_modify
  2469. {
  2470.     my ( $self, $session, $mail, $client, $nosave, $class, $slot, $echo, $crlf ) = @_;
  2471.  
  2472.     $echo = 1    unless (defined $echo);
  2473.     $crlf = $eol unless (defined $crlf);
  2474.  
  2475.     my $msg_subject;              # The message subject
  2476.     my $msg_head_before = '';     # Store the message headers that
  2477.                                   # come before Subject here
  2478.     my $msg_head_after = '';      # Store the message headers that
  2479.                                   # come after Subject here
  2480.     my $msg_head_q      = '';     # Store questionable header lines here
  2481.     my $msg_body        = '';     # Store the message body here
  2482.     my $in_subject_header = 0;    # 1 if in Subject header
  2483.  
  2484.     # These two variables are used to control the insertion of the
  2485.     # X-POPFile-TimeoutPrevention header when downloading long or slow
  2486.     # emails
  2487.  
  2488.     my $last_timeout   = time;
  2489.     my $timeout_count  = 0;
  2490.  
  2491.     # Indicates whether the first time through the receive loop we got
  2492.     # the full body, this will happen on small emails
  2493.  
  2494.     my $got_full_body  = 0;
  2495.  
  2496.     # The size of the message downloaded so far.
  2497.  
  2498.     my $message_size   = 0;
  2499.  
  2500.     # The classification for this message
  2501.  
  2502.     my $classification = '';
  2503.  
  2504.     # Whether we are currently reading the mail headers or not
  2505.  
  2506.     my $getting_headers = 1;
  2507.  
  2508.     my $msg_file;
  2509.  
  2510.     # If we don't yet know the classification then start the parser
  2511.  
  2512.     $class = '' if ( !defined( $class ) );
  2513.     if ( $class eq '' ) {
  2514.         $self->{parser__}->start_parse();
  2515.         ( $slot, $msg_file ) = $self->{history__}->reserve_slot();
  2516.     } else {
  2517.         $msg_file = $self->{history__}->get_slot_file( $slot );
  2518.     }
  2519.  
  2520.     # We append .TMP to the filename for the MSG file so that if we are in
  2521.     # middle of downloading a message and we refresh the history we do not
  2522.     # get class file errors
  2523.  
  2524.     open MSG, ">$msg_file" unless $nosave;
  2525.  
  2526.     while ( my $line = $self->slurp_( $mail ) ) {
  2527.         my $fileline;
  2528.  
  2529.         # This is done so that we remove the network style end of line
  2530.         # CR LF and allow Perl to decide on the local system EOL which
  2531.         # it will expand out of \n when this gets written to the temp
  2532.         # file
  2533.  
  2534.         $fileline = $line;
  2535.         $fileline =~ s/[\r\n]//g;
  2536.         $fileline .= "\n";
  2537.  
  2538.         # Check for an abort
  2539.  
  2540.         last if ( $self->{alive_} == 0 );
  2541.  
  2542.         # The termination of a message is a line consisting of exactly
  2543.         # .CRLF so we detect that here exactly
  2544.  
  2545.         if ( $line =~ /^\.(\r\n|\r|\n)$/ ) {
  2546.             $got_full_body = 1;
  2547.             last;
  2548.         }
  2549.  
  2550.         if ( $getting_headers )  {
  2551.  
  2552.             # Kill header lines containing only whitespace (Exim does this)
  2553.  
  2554.             next if ( $line =~ /^[ \t]+(\r\n|\r|\n)$/i );
  2555.  
  2556.             if ( !( $line =~ /^(\r\n|\r|\n)$/i ) )  {
  2557.                 $message_size += length $line;
  2558.                 $self->write_line__( $nosave?undef:\*MSG, $fileline, $class );
  2559.  
  2560.                 # If there is no echoing occuring, it doesn't matter
  2561.                 # what we do to these
  2562.  
  2563.                 if ( $echo ) {
  2564.                     if ( $line =~ /^Subject:(.*)/i )  {
  2565.                         $msg_subject = $1;
  2566.                         $msg_subject =~ s/(\012|\015)//g;
  2567.                         $in_subject_header = 1;
  2568.                         next;
  2569.                     } elsif ( $line !~ /^[ \t]/ ) {
  2570.                         $in_subject_header = 0;
  2571.                     }
  2572.  
  2573.                     # Strip out the X-Text-Classification header that
  2574.                     # is in an incoming message
  2575.  
  2576.                     next if ( $line =~ /^X-Text-Classification:/i );
  2577.                     next if ( $line =~ /^X-POPFile-Link:/i );
  2578.  
  2579.                     # Store any lines that appear as though they may
  2580.                     # be non-header content Lines that are headers
  2581.                     # begin with whitespace or Alphanumerics and "-"
  2582.                     # followed by a colon.
  2583.                     #
  2584.                     # This prevents weird things like HTML before the
  2585.                     # headers terminate from causing the XPL and XTC
  2586.                     # headers to be inserted in places some clients
  2587.                     # can't detect
  2588.  
  2589.                     if ( ( $line =~ /^[ \t]/ ) && $in_subject_header ) {
  2590.                         $line =~ s/(\012|\015)//g;
  2591.                         $msg_subject .= $crlf . $line;
  2592.                         next;
  2593.                     }
  2594.  
  2595.                     if ( $line =~ /^([ \t]|([A-Z0-9\-_]+:))/i ) {
  2596.                         if ( !defined($msg_subject) )  {
  2597.                             $msg_head_before .= $msg_head_q . $line;
  2598.                         } else {
  2599.                             $msg_head_after  .= $msg_head_q . $line;
  2600.                         }
  2601.                         $msg_head_q = '';
  2602.                     } else {
  2603.  
  2604.                         # Gather up any header lines that are questionable
  2605.  
  2606.                         $self->log_( 1, "Found odd email header: $line" );
  2607.                         $msg_head_q .= $line;
  2608.                     }
  2609.                 }
  2610.             } else {
  2611.                 $self->write_line__( $nosave?undef:\*MSG, "\n", $class );
  2612.                 $message_size += length $crlf;
  2613.                 $getting_headers = 0;
  2614.             }
  2615.         } else {
  2616.             $message_size += length $line;
  2617.             $msg_body     .= $line;
  2618.             $self->write_line__( $nosave?undef:\*MSG, $fileline, $class );
  2619.         }
  2620.  
  2621.         # Check to see if too much time has passed and we need to keep
  2622.         # the mail client happy
  2623.  
  2624.         if ( time > ( $last_timeout + 2 ) ) {
  2625.             print $client "X-POPFile-TimeoutPrevention: $timeout_count$crlf" if ( $echo );
  2626.             $timeout_count += 1;
  2627.             $last_timeout = time;
  2628.         }
  2629.  
  2630.         last if ( ( $message_size > $self->global_config_( 'message_cutoff' ) ) && ( $getting_headers == 0 ) );
  2631.     }
  2632.  
  2633.     close MSG unless $nosave;
  2634.  
  2635.     # If we don't yet know the classification then stop the parser
  2636.     if ( $class eq '' ) {
  2637.         $self->{parser__}->stop_parse();
  2638.     }
  2639.  
  2640.     # Do the text classification and update the counter for that
  2641.     # bucket that we just downloaded an email of that type
  2642.  
  2643.     $classification = ($class ne '')?$class:$self->classify( $session, undef);
  2644.  
  2645.     my $subject_modification = $self->get_bucket_parameter( $session, $classification, 'subject'    );
  2646.     my $xtc_insertion        = $self->get_bucket_parameter( $session, $classification, 'xtc'        );
  2647.     my $xpl_insertion        = $self->get_bucket_parameter( $session, $classification, 'xpl'        );
  2648.     my $quarantine           = $self->get_bucket_parameter( $session, $classification, 'quarantine' );
  2649.  
  2650.     my $modification = $self->config_( 'subject_mod_left' ) . $classification . $self->config_( 'subject_mod_right' );
  2651.  
  2652.     # Add the Subject line modification or the original line back again
  2653.     # Don't add the classification unless it is not present
  2654.  
  2655.     if (  ( defined( $msg_subject ) && ( $msg_subject !~ /\Q$modification\E/ ) ) && # PROFILE BLOCK START
  2656.           ( $subject_modification == 1 ) &&
  2657.           ( $quarantine == 0 ) )  {                                                 # PROFILE BLOCK STOP
  2658.          $msg_subject = " $modification$msg_subject";
  2659.     }
  2660.  
  2661.     if ( !defined( $msg_subject )       &&                                         # PROFILE BLOCK START
  2662.          ( $subject_modification == 1 ) &&
  2663.          ( $quarantine == 0 ) )  {                                                 # PROFILE BLOCK STOP
  2664.          $msg_subject = " $modification";
  2665.     }
  2666.  
  2667.     $msg_subject = '' if ( !defined( $msg_subject ) );
  2668.  
  2669.     $msg_head_before .= 'Subject:' . $msg_subject;
  2670.     $msg_head_before .= $crlf;
  2671.  
  2672.     # Add the XTC header
  2673.     $msg_head_after .= "X-Text-Classification: $classification$crlf" if ( ( $xtc_insertion   ) && # PROFILE BLOCK START
  2674.                                                                           ( $quarantine == 0 ) ); # PROFILE BLOCK STOP
  2675.  
  2676.     # Add the XPL header
  2677.  
  2678.     my $xpl = $self->config_( 'xpl_angle' )?'<':'';
  2679.  
  2680.     $xpl .= "http://";
  2681.     $xpl .= $self->module_config_( 'html', 'local' )?"127.0.0.1":$self->config_( 'hostname' );
  2682.     $xpl .= ":" . $self->module_config_( 'html', 'port' ) . "/jump_to_message?view=$slot";
  2683.  
  2684.     if ( $self->config_( 'xpl_angle' ) ) {
  2685.         $xpl .= '>';
  2686.     }
  2687.  
  2688.     $xpl .= "$crlf";
  2689.  
  2690.     if ( $xpl_insertion && ( $quarantine == 0 ) ) {
  2691.         $msg_head_after .= 'X-POPFile-Link: ' . $xpl;
  2692.     }
  2693.  
  2694.     $msg_head_after .= $msg_head_q . "$crlf";
  2695.  
  2696.     # Echo the text of the message to the client
  2697.  
  2698.     if ( $echo ) {
  2699.  
  2700.         # If the bucket is quarantined then we'll treat it specially
  2701.         # by changing the message header to contain information from
  2702.         # POPFile and wrapping the original message in a MIME encoding
  2703.  
  2704.        if ( $quarantine == 1 ) {
  2705.            my ( $orig_from, $orig_to, $orig_subject ) = ( $self->{parser__}->get_header('from'), $self->{parser__}->get_header('to'), $self->{parser__}->get_header('subject') );
  2706.            my ( $encoded_from, $encoded_to ) = ( $orig_from, $orig_to );
  2707.            if ( $self->{parser__}->{lang__} eq 'Nihongo' ) {
  2708.                require Encode;
  2709.  
  2710.                Encode::from_to( $orig_from, 'euc-jp', 'iso-2022-jp');
  2711.                Encode::from_to( $orig_to, 'euc-jp', 'iso-2022-jp');
  2712.                Encode::from_to( $orig_subject, 'euc-jp', 'iso-2022-jp');
  2713.  
  2714.                $encoded_from = $orig_from;
  2715.                $encoded_to = $orig_to;
  2716.                $encoded_from =~ s/(\x1B\x24\x42.+\x1B\x28\x42)/"=?ISO-2022-JP?B?" . encode_base64($1,'') . "?="/eg;
  2717.                $encoded_to =~ s/(\x1B\x24\x42.+\x1B\x28\x42)/"=?ISO-2022-JP?B?" . encode_base64($1,'') . "?="/eg;
  2718.            }
  2719.  
  2720.            print $client "From: $encoded_from$crlf";
  2721.            print $client "To: $encoded_to$crlf";
  2722.            print $client "Date: " . $self->{parser__}->get_header( 'date' ) . "$crlf";
  2723.            # Don't add the classification unless it is not present
  2724.            if ( ( defined( $msg_subject ) && ( $msg_subject !~ /\[\Q$classification\E\]/ ) ) && # PROFILE BLOCK START
  2725.                  ( $subject_modification == 1 ) ) {                                             # PROFILE BLOCK STOP
  2726.                $msg_subject = " $modification$msg_subject";
  2727.            }
  2728.            print $client "Subject:$msg_subject$crlf";
  2729.            print $client "X-Text-Classification: $classification$crlf" if ( $xtc_insertion );
  2730.            print $client 'X-POPFile-Link: ' . $xpl if ( $xpl_insertion );
  2731.            print $client "MIME-Version: 1.0$crlf";
  2732.            print $client "Content-Type: multipart/report; boundary=\"$slot\"$crlf$crlf--$slot$crlf";
  2733.            print $client "Content-Type: text/plain";
  2734.            print $client "; charset=iso-2022-jp" if ( $self->{parser__}->{lang__} eq 'Nihongo' );
  2735.            print $client "$crlf$crlf";
  2736.            print $client "POPFile has quarantined a message.  It is attached to this email.$crlf$crlf";
  2737.            print $client "Quarantined Message Detail$crlf$crlf";
  2738.  
  2739.            print $client "Original From: $orig_from$crlf";
  2740.            print $client "Original To: $orig_to$crlf";
  2741.            print $client "Original Subject: $orig_subject$crlf";
  2742.  
  2743.            print $client "To examine the email open the attachment. ";
  2744.            print $client "To change this mail's classification go to $xpl";
  2745.            print $client "$crlf";
  2746.            print $client "The first 20 words found in the email are:$crlf$crlf";
  2747.  
  2748.            my $first20 = $self->{parser__}->first20();
  2749.            if ( $self->{parser__}->{lang__} eq 'Nihongo' ) {
  2750.                require Encode;
  2751.  
  2752.                Encode::from_to( $first20, 'euc-jp', 'iso-2022-jp');
  2753.            }
  2754.  
  2755.            print $client $first20;
  2756.            print $client "$crlf--$slot$crlf";
  2757.            print $client "Content-Type: message/rfc822$crlf$crlf";
  2758.         }
  2759.  
  2760.         print $client $msg_head_before;
  2761.         print $client $msg_head_after;
  2762.         print $client $msg_body;
  2763.     }
  2764.  
  2765.     my $before_dot = '';
  2766.  
  2767.     if ( $quarantine && $echo ) {
  2768.         $before_dot = "$crlf--$slot--$crlf";
  2769.     }
  2770.  
  2771.     my $need_dot = 0;
  2772.  
  2773.     if ( $got_full_body ) {
  2774.         $need_dot = 1;
  2775.     } else {
  2776.         $need_dot = !$self->echo_to_dot_( $mail, $echo?$client:undef, $nosave?undef:'>>' . $msg_file, $before_dot ) && !$nosave;
  2777.     }
  2778.  
  2779.     if ( $need_dot ) {
  2780.         print $client $before_dot if ( $before_dot ne '' );
  2781.         print $client ".$crlf"    if ( $echo );
  2782.     }
  2783.  
  2784.     # In some cases it's possible (and totally illegal) to get a . in
  2785.     # the middle of the message, to cope with the we call flush_extra_
  2786.     # here to remove any extra stuff the POP3 server is sending Make
  2787.     # sure to supress output if we are not echoing, and to save to
  2788.     # file if not echoing and saving
  2789.  
  2790.     if ( !($nosave || $echo) ) {
  2791.  
  2792.         # if we're saving (not nosave) and not echoing, we can safely
  2793.         # unload this into the temp file
  2794.  
  2795.         if (open FLUSH, ">$msg_file.flush") {
  2796.             binmode FLUSH;
  2797.  
  2798.             # TODO: Do this in a faster way (without flushing to one
  2799.             # file then copying to another) (perhaps a select on $mail
  2800.             # to predict if there is flushable data)
  2801.  
  2802.             $self->flush_extra_( $mail, \*FLUSH, 0);
  2803.             close FLUSH;
  2804.  
  2805.             # append any data we got to the actual temp file
  2806.  
  2807.             if ( ( (-s "$msg_file.flush") > 0 ) && ( open FLUSH, "<$msg_file.flush" ) ) {
  2808.                 binmode FLUSH;
  2809.                 if ( open TEMP, ">>$msg_file" ) {
  2810.                     binmode TEMP;
  2811.  
  2812.                     # The only time we get data here is if it is after
  2813.                     # a CRLF.CRLF We have to re-create it to avoid
  2814.                     # data-loss
  2815.  
  2816.                     print TEMP ".$crlf";
  2817.  
  2818.                     print TEMP $_ while (<FLUSH>);
  2819.  
  2820.                     # NOTE: The last line flushed MAY be a CRLF.CRLF,
  2821.                     # which isn't actually part of the message body
  2822.  
  2823.                     close TEMP;
  2824.                 }
  2825.                 close FLUSH;
  2826.             }
  2827.             unlink("$msg_file.flush");
  2828.         }
  2829.     } else {
  2830.  
  2831.         # if we are echoing, the client can make sure we have no data
  2832.         # loss otherwise, the data can be discarded (not saved and not
  2833.         # echoed)
  2834.  
  2835.         $self->flush_extra_( $mail, $client, $echo?0:1);
  2836.     }
  2837.  
  2838.     if ( $class eq '' ) {
  2839.         if ( $nosave ) {
  2840.             $self->{history__}->release_slot( $slot );
  2841.         } else {
  2842.             $self->{history__}->commit_slot( $session, $slot, $classification, $self->{magnet_detail__} );
  2843.         }
  2844.     }
  2845.  
  2846.     return ( $classification, $slot, $self->{magnet_used__} );
  2847. }
  2848.  
  2849. #----------------------------------------------------------------------------
  2850. #
  2851. # get_buckets
  2852. #
  2853. # Returns a list containing all the real bucket names sorted into
  2854. # alphabetic order
  2855. #
  2856. # $session   A valid session key returned by a call to get_session_key
  2857. #
  2858. #----------------------------------------------------------------------------
  2859. sub get_buckets
  2860. {
  2861.     my ( $self, $session ) = @_;
  2862.  
  2863.     my $userid = $self->valid_session_key__( $session );
  2864.     return undef if ( !defined( $userid ) );
  2865.  
  2866.     # Note that get_buckets does not return pseudo buckets
  2867.  
  2868.     my @buckets;
  2869.  
  2870.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2871.         if ( $self->{db_bucketid__}{$userid}{$b}{pseudo} == 0 ) {
  2872.             push @buckets, ($b);
  2873.         }
  2874.     }
  2875.  
  2876.     return @buckets;
  2877. }
  2878.  
  2879. #----------------------------------------------------------------------------
  2880. #
  2881. # get_bucket_id
  2882. #
  2883. # Returns the internal ID for a bucket for database calls
  2884. #
  2885. # $session   A valid session key returned by a call to get_session_key
  2886. # $bucket    The bucket name
  2887. #
  2888. #----------------------------------------------------------------------------
  2889. sub get_bucket_id
  2890. {
  2891.     my ( $self, $session, $bucket ) = @_;
  2892.  
  2893.     my $userid = $self->valid_session_key__( $session );
  2894.     return undef if ( !defined( $userid ) );
  2895.  
  2896.     return $self->{db_bucketid__}{$userid}{$bucket}{id};
  2897. }
  2898.  
  2899. #----------------------------------------------------------------------------
  2900. #
  2901. # get_bucket_name
  2902. #
  2903. # Returns the name of a bucket from an internal ID
  2904. #
  2905. # $session   A valid session key returned by a call to get_session_key
  2906. # $id        The bucket id
  2907. #
  2908. #----------------------------------------------------------------------------
  2909. sub get_bucket_name
  2910. {
  2911.     my ( $self, $session, $id ) = @_;
  2912.  
  2913.     my $userid = $self->valid_session_key__( $session );
  2914.     return undef if ( !defined( $userid ) );
  2915.  
  2916.     foreach $b (keys %{$self->{db_bucketid__}{$userid}}) {
  2917.         if ( $id == $self->{db_bucketid__}{$userid}{$b}{id} ) {
  2918.             return $b;
  2919.         }
  2920.     }
  2921.  
  2922.     return '';
  2923. }
  2924.  
  2925. #----------------------------------------------------------------------------
  2926. #
  2927. # get_pseudo_buckets
  2928. #
  2929. # Returns a list containing all the pseudo bucket names sorted into
  2930. # alphabetic order
  2931. #
  2932. # $session   A valid session key returned by a call to get_session_key
  2933. #
  2934. #----------------------------------------------------------------------------
  2935. sub get_pseudo_buckets
  2936. {
  2937.     my ( $self, $session ) = @_;
  2938.  
  2939.     my $userid = $self->valid_session_key__( $session );
  2940.     return undef if ( !defined( $userid ) );
  2941.  
  2942.     my @buckets;
  2943.  
  2944.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2945.         if ( $self->{db_bucketid__}{$userid}{$b}{pseudo} == 1 ) {
  2946.             push @buckets, ($b);
  2947.         }
  2948.     }
  2949.  
  2950.     return @buckets;
  2951. }
  2952.  
  2953. #----------------------------------------------------------------------------
  2954. #
  2955. # get_all_buckets
  2956. #
  2957. # Returns a list containing all the bucket names sorted into
  2958. # alphabetic order
  2959. #
  2960. # $session   A valid session key returned by a call to get_session_key
  2961. #
  2962. #----------------------------------------------------------------------------
  2963. sub get_all_buckets
  2964. {
  2965.     my ( $self, $session ) = @_;
  2966.  
  2967.     my $userid = $self->valid_session_key__( $session );
  2968.     return undef if ( !defined( $userid ) );
  2969.  
  2970.     my @buckets;
  2971.  
  2972.     for my $b (sort keys %{$self->{db_bucketid__}{$userid}}) {
  2973.          push @buckets, ($b);
  2974.     }
  2975.  
  2976.     return @buckets;
  2977. }
  2978.  
  2979. #----------------------------------------------------------------------------
  2980. #
  2981. # is_pseudo_bucket
  2982. #
  2983. # Returns 1 if the named bucket is pseudo
  2984. #
  2985. # $session   A valid session key returned by a call to get_session_key
  2986. # $bucket    The bucket to check
  2987. #
  2988. #----------------------------------------------------------------------------
  2989. sub is_pseudo_bucket
  2990. {
  2991.     my ( $self, $session, $bucket ) = @_;
  2992.  
  2993.     my $userid = $self->valid_session_key__( $session );
  2994.     return undef if ( !defined( $userid ) );
  2995.  
  2996.     return ( defined($self->{db_bucketid__}{$userid}{$bucket})   # PROFILE BLOCK START
  2997.           && $self->{db_bucketid__}{$userid}{$bucket}{pseudo} ); # PROFILE BLOCK STOP
  2998. }
  2999.  
  3000. #----------------------------------------------------------------------------
  3001. #
  3002. # is_bucket
  3003. #
  3004. # Returns 1 if the named bucket is a bucket
  3005. #
  3006. # $session   A valid session key returned by a call to get_session_key
  3007. # $bucket    The bucket to check
  3008. #
  3009. #----------------------------------------------------------------------------
  3010. sub is_bucket
  3011. {
  3012.     my ( $self, $session, $bucket ) = @_;
  3013.  
  3014.     my $userid = $self->valid_session_key__( $session );
  3015.     return undef if ( !defined( $userid ) );
  3016.  
  3017.     return ( ( defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) &&  # PROFILE BLOCK START
  3018.              ( !$self->{db_bucketid__}{$userid}{$bucket}{pseudo} ) );    # PROFILE BLOCK STOP
  3019. }
  3020.  
  3021. #----------------------------------------------------------------------------
  3022. #
  3023. # get_bucket_word_count
  3024. #
  3025. # Returns the total word count (including duplicates) for the passed in bucket
  3026. #
  3027. # $session     A valid session key returned by a call to get_session_key
  3028. # $bucket      The name of the bucket for which the word count is desired
  3029. #
  3030. #----------------------------------------------------------------------------
  3031. sub get_bucket_word_count
  3032. {
  3033.     my ( $self, $session, $bucket ) = @_;
  3034.  
  3035.     my $userid = $self->valid_session_key__( $session );
  3036.     return undef if ( !defined( $userid ) );
  3037.  
  3038.     my $c = $self->{db_bucketcount__}{$userid}{$bucket};
  3039.  
  3040.     return defined($c)?$c:0;
  3041. }
  3042.  
  3043. #----------------------------------------------------------------------------
  3044. #
  3045. # get_bucket_word_list
  3046. #
  3047. # Returns a list of words all with the same first character
  3048. #
  3049. # $session     A valid session key returned by a call to get_session_key
  3050. # $bucket      The name of the bucket for which the word count is desired
  3051. # $prefix      The first character of the words
  3052. #
  3053. #----------------------------------------------------------------------------
  3054. sub get_bucket_word_list
  3055. {
  3056.     my ( $self, $session, $bucket, $prefix ) = @_;
  3057.  
  3058.     my $userid = $self->valid_session_key__( $session );
  3059.     return undef if ( !defined( $userid ) );
  3060.  
  3061.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3062.     my $result = $self->{db__}->selectcol_arrayref(  # PROFILE BLOCK START
  3063.         "select words.word from matrix, words
  3064.          where matrix.wordid  = words.id and
  3065.                matrix.bucketid = $bucketid and
  3066.                words.word like '$prefix%';");        # PROFILE BLOCK STOP
  3067.  
  3068.     return @{$result};
  3069. }
  3070.  
  3071. #----------------------------------------------------------------------------
  3072. #
  3073. # get_bucket_word_prefixes
  3074. #
  3075. # Returns a list of all the initial letters of words in a bucket
  3076. #
  3077. # $session     A valid session key returned by a call to get_session_key
  3078. # $bucket      The name of the bucket for which the word count is desired
  3079. #
  3080. #----------------------------------------------------------------------------
  3081. sub get_bucket_word_prefixes
  3082. {
  3083.     my ( $self, $session, $bucket ) = @_;
  3084.  
  3085.     my $userid = $self->valid_session_key__( $session );
  3086.     return undef if ( !defined( $userid ) );
  3087.  
  3088.     my $prev = '';
  3089.  
  3090.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3091.     my $result = $self->{db__}->selectcol_arrayref(   # PROFILE BLOCK START
  3092.         "select words.word from matrix, words
  3093.          where matrix.wordid  = words.id and
  3094.                matrix.bucketid = $bucketid;");        # PROFILE BLOCK STOP
  3095.  
  3096.     # In Japanese mode, disable locale and use substr_euc, the substr
  3097.     # function which supports EUC Japanese charset.  Sorting Japanese
  3098.     # with "use locale" is memory and time consuming, and may cause
  3099.     # perl crash.
  3100.  
  3101.     if ( $self->module_config_( 'html', 'language' ) eq 'Nihongo' ) {
  3102.         return grep {$_ ne $prev && ($prev = $_, 1)} sort map {substr_euc__($_,0,1)} @{$result};
  3103.     } else {
  3104.         if  ( $self->module_config_( 'html', 'language' ) eq 'Korean' ) {
  3105.             return grep {$_ ne $prev && ($prev = $_, 1)} sort map {$_ =~ /([\x20-\x80]|$eksc)/} @{$result};
  3106.         } else {
  3107.             return grep {$_ ne $prev && ($prev = $_, 1)} sort map {substr($_,0,1)}  @{$result};
  3108.         }
  3109.     }
  3110. }
  3111.  
  3112. #----------------------------------------------------------------------------
  3113. #
  3114. # get_word_count
  3115. #
  3116. # Returns the total word count (including duplicates)
  3117. #
  3118. # $session   A valid session key returned by a call to get_session_key
  3119. #
  3120. #----------------------------------------------------------------------------
  3121. sub get_word_count
  3122. {
  3123.     my ( $self, $session ) = @_;
  3124.  
  3125.     my $userid = $self->valid_session_key__( $session );
  3126.     return undef if ( !defined( $userid ) );
  3127.  
  3128.     $self->{db_get_full_total__}->execute( $userid );
  3129.     return $self->{db_get_full_total__}->fetchrow_arrayref->[0];
  3130. }
  3131.  
  3132. #----------------------------------------------------------------------------
  3133. #
  3134. # get_count_for_word
  3135. #
  3136. # Returns the number of times the word occurs in a bucket
  3137. #
  3138. # $session         A valid session key returned by a call to get_session_key
  3139. # $bucket          The bucket we are asking about
  3140. # $word            The word we are asking about
  3141. #
  3142. #----------------------------------------------------------------------------
  3143. sub get_count_for_word
  3144. {
  3145.     my ( $self, $session, $bucket, $word ) = @_;
  3146.  
  3147.     my $userid = $self->valid_session_key__( $session );
  3148.     return undef if ( !defined( $userid ) );
  3149.  
  3150.     return $self->get_base_value_( $session, $bucket, $word );
  3151. }
  3152.  
  3153. #----------------------------------------------------------------------------
  3154. #
  3155. # get_bucket_unique_count
  3156. #
  3157. # Returns the unique word count (excluding duplicates) for the passed
  3158. # in bucket
  3159. #
  3160. # $session     A valid session key returned by a call to get_session_key
  3161. # $bucket      The name of the bucket for which the word count is desired
  3162. #
  3163. #----------------------------------------------------------------------------
  3164. sub get_bucket_unique_count
  3165. {
  3166.     my ( $self, $session, $bucket ) = @_;
  3167.  
  3168.     my $userid = $self->valid_session_key__( $session );
  3169.     return undef if ( !defined( $userid ) );
  3170.  
  3171.     my $c = $self->{db_bucketunique__}{$userid}{$bucket};
  3172.  
  3173.     return defined($c)?$c:0;
  3174. }
  3175.  
  3176. #----------------------------------------------------------------------------
  3177. #
  3178. # get_unique_word_count
  3179. #
  3180. # Returns the unique word count (excluding duplicates) for all buckets
  3181. #
  3182. # $session   A valid session key returned by a call to get_session_key
  3183. #
  3184. #----------------------------------------------------------------------------
  3185. sub get_unique_word_count
  3186. {
  3187.     my ( $self, $session ) = @_;
  3188.  
  3189.     my $userid = $self->valid_session_key__( $session );
  3190.     return undef if ( !defined( $userid ) );
  3191.  
  3192.     $self->{db_get_unique_word_count__}->execute( $userid );
  3193.     return $self->{db_get_unique_word_count__}->fetchrow_arrayref->[0];
  3194. }
  3195.  
  3196. #----------------------------------------------------------------------------
  3197. #
  3198. # get_bucket_color
  3199. #
  3200. # Returns the color associated with a bucket
  3201. #
  3202. # $session   A valid session key returned by a call to get_session_key
  3203. # $bucket      The name of the bucket for which the color is requested
  3204. #
  3205. # NOTE  This API is DEPRECATED in favor of calling get_bucket_parameter for
  3206. #       the parameter named 'color'
  3207. #----------------------------------------------------------------------------
  3208. sub get_bucket_color
  3209. {
  3210.     my ( $self, $session, $bucket ) = @_;
  3211.  
  3212.     return $self->get_bucket_parameter( $session, $bucket, 'color' );
  3213. }
  3214.  
  3215. #----------------------------------------------------------------------------
  3216. #
  3217. # set_bucket_color
  3218. #
  3219. # Returns the color associated with a bucket
  3220. #
  3221. # $session     A valid session key returned by a call to get_session_key
  3222. # $bucket      The name of the bucket for which the color is requested
  3223. # $color       The new color
  3224. #
  3225. # NOTE  This API is DEPRECATED in favor of calling set_bucket_parameter for
  3226. #       the parameter named 'color'
  3227. #----------------------------------------------------------------------------
  3228. sub set_bucket_color
  3229. {
  3230.     my ( $self, $session, $bucket, $color ) = @_;
  3231.  
  3232.     return $self->set_bucket_parameter( $session, $bucket, 'color', $color );
  3233. }
  3234.  
  3235. #----------------------------------------------------------------------------
  3236. #
  3237. # get_bucket_parameter
  3238. #
  3239. # Returns the value of a per bucket parameter
  3240. #
  3241. # $session     A valid session key returned by a call to get_session_key
  3242. # $bucket      The name of the bucket
  3243. # $parameter   The name of the parameter
  3244. #
  3245. #----------------------------------------------------------------------------
  3246. sub get_bucket_parameter
  3247. {
  3248.     my ( $self, $session, $bucket, $parameter ) = @_;
  3249.  
  3250.     my $userid = $self->valid_session_key__( $session );
  3251.     return undef if ( !defined( $userid ) );
  3252.  
  3253.     # See if there's a cached value
  3254.  
  3255.     if ( defined( $self->{db_parameters__}{$userid}{$bucket}{$parameter} ) ) {
  3256.         return $self->{db_parameters__}{$userid}{$bucket}{$parameter};
  3257.     }
  3258.  
  3259.     # Make sure that the bucket passed in actually exists
  3260.  
  3261.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3262.         return undef;
  3263.     }
  3264.  
  3265.     # If there is a non-default value for this parameter then return it.
  3266.  
  3267.     $self->{db_get_bucket_parameter__}->execute( $self->{db_bucketid__}{$userid}{$bucket}{id}, $self->{db_parameterid__}{$parameter} );
  3268.     my $result = $self->{db_get_bucket_parameter__}->fetchrow_arrayref;
  3269.  
  3270.     # If this parameter has not been defined for this specific bucket then
  3271.     # get the default value
  3272.  
  3273.     if ( !defined( $result ) ) {
  3274.         $self->{db_get_bucket_parameter_default__}->execute(  # PROFILE BLOCK START
  3275.             $self->{db_parameterid__}{$parameter} );          # PROFILE BLOCK STOP
  3276.         $result = $self->{db_get_bucket_parameter_default__}->fetchrow_arrayref;
  3277.     }
  3278.  
  3279.     if ( defined( $result ) ) {
  3280.         $self->{db_parameters__}{$userid}{$bucket}{$parameter} = $result->[0];
  3281.         return $result->[0];
  3282.     } else {
  3283.         return undef;
  3284.     }
  3285. }
  3286.  
  3287. #----------------------------------------------------------------------------
  3288. #
  3289. # set_bucket_parameter
  3290. #
  3291. # Sets the value associated with a bucket specific parameter
  3292. #
  3293. # $session     A valid session key returned by a call to get_session_key
  3294. # $bucket      The name of the bucket
  3295. # $parameter   The name of the parameter
  3296. # $value       The new value
  3297. #
  3298. #----------------------------------------------------------------------------
  3299. sub set_bucket_parameter
  3300. {
  3301.     my ( $self, $session, $bucket, $parameter, $value ) = @_;
  3302.  
  3303.     my $userid = $self->valid_session_key__( $session );
  3304.     return undef if ( !defined( $userid ) );
  3305.  
  3306.     # Make sure that the bucket passed in actually exists
  3307.  
  3308.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3309.         return undef;
  3310.     }
  3311.  
  3312.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3313.     my $btid     = $self->{db_parameterid__}{$parameter};
  3314.  
  3315.     # Exactly one row should be affected by this statement
  3316.  
  3317.     $self->{db_set_bucket_parameter__}->execute( $bucketid, $btid, $value );
  3318.  
  3319.     if ( defined( $self->{db_parameters__}{$userid}{$bucket}{$parameter} ) ) {
  3320.         $self->{db_parameters__}{$userid}{$bucket}{$parameter} = $value;
  3321.     }
  3322.  
  3323.     return 1;
  3324. }
  3325.  
  3326. #----------------------------------------------------------------------------
  3327. #
  3328. # get_html_colored_message
  3329. #
  3330. # Parser a mail message stored in a file and returns HTML representing
  3331. # the message with coloring of the words
  3332. #
  3333. # $session        A valid session key returned by a call to get_session_key
  3334. # $file           The file to parse
  3335. #
  3336. #----------------------------------------------------------------------------
  3337. sub get_html_colored_message
  3338. {
  3339.     my ( $self, $session, $file ) = @_;
  3340.  
  3341.     my $userid = $self->valid_session_key__( $session );
  3342.     return undef if ( !defined( $userid ) );
  3343.  
  3344.     $self->{parser__}->{color__} = $session;
  3345.     $self->{parser__}->{color_matrix__} = undef;
  3346.     $self->{parser__}->{color_idmap__}  = undef;
  3347.     $self->{parser__}->{color_userid__} = undef;
  3348.     $self->{parser__}->{bayes__} = bless $self;
  3349.     
  3350.     my $result = $self->{parser__}->parse_file( $file,   # PROFILE BLOCK START
  3351.            $self->global_config_( 'message_cutoff'   ) ); # PROFILE BLOCK STOP
  3352.  
  3353.     $self->{parser__}->{color__} = '';
  3354.  
  3355.     return $result;
  3356. }
  3357.  
  3358. #----------------------------------------------------------------------------
  3359. #
  3360. # fast_get_html_colored_message
  3361. #
  3362. # Parser a mail message stored in a file and returns HTML representing the message
  3363. # with coloring of the words
  3364. #
  3365. # $session        A valid session key returned by a call to get_session_key
  3366. # $file           The file to colorize
  3367. # $matrix         Reference to the matrix hash from a call to classify
  3368. # $idmap          Reference to the idmap hash from a call to classify
  3369. #
  3370. #----------------------------------------------------------------------------
  3371. sub fast_get_html_colored_message
  3372. {
  3373.     my ( $self, $session, $file, $matrix, $idmap ) = @_;
  3374.  
  3375.     my $userid = $self->valid_session_key__( $session );
  3376.     return undef if ( !defined( $userid ) );
  3377.  
  3378.     $self->{parser__}->{color__}        = $session;
  3379.     $self->{parser__}->{color_matrix__} = $matrix;
  3380.     $self->{parser__}->{color_idmap__}  = $idmap;
  3381.     $self->{parser__}->{color_userid__} = $userid;
  3382.     $self->{parser__}->{bayes__}        = bless $self;
  3383.  
  3384.     my $result = $self->{parser__}->parse_file( $file,
  3385.                                                 $self->global_config_( 'message_cutoff'   ) );
  3386.  
  3387.     $self->{parser__}->{color__} = '';
  3388.  
  3389.     return $result;
  3390. }
  3391.  
  3392. #----------------------------------------------------------------------------
  3393. #
  3394. # create_bucket
  3395. #
  3396. # Creates a new bucket, returns 1 if the creation succeeded
  3397. #
  3398. # $session     A valid session key returned by a call to get_session_key
  3399. # $bucket      Name for the new bucket
  3400. #
  3401. #----------------------------------------------------------------------------
  3402. sub create_bucket
  3403. {
  3404.     my ( $self, $session, $bucket ) = @_;
  3405.  
  3406.     if ( $self->is_bucket( $session, $bucket ) ||           # PROFILE BLOCK START
  3407.          $self->is_pseudo_bucket( $session, $bucket ) ) {   # PROFILE BLOCK STOP
  3408.         return 0;
  3409.     }
  3410.  
  3411.     my $userid = $self->valid_session_key__( $session );
  3412.     return undef if ( !defined( $userid ) );
  3413.  
  3414.     $bucket = $self->{db__}->quote( $bucket );
  3415.  
  3416.     $self->{db__}->do(                                                                    # PROFILE BLOCK START
  3417.         "insert into buckets ( name, pseudo, userid ) values ( $bucket, 0, $userid );" ); # PROFILE BLOCK STOP
  3418.     $self->db_update_cache__( $session );
  3419.  
  3420.     return 1;
  3421. }
  3422.  
  3423. #----------------------------------------------------------------------------
  3424. #
  3425. # delete_bucket
  3426. #
  3427. # Deletes a bucket, returns 1 if the delete succeeded
  3428. #
  3429. # $session     A valid session key returned by a call to get_session_key
  3430. # $bucket      Name of the bucket to delete
  3431. #
  3432. #----------------------------------------------------------------------------
  3433. sub delete_bucket
  3434. {
  3435.     my ( $self, $session, $bucket ) = @_;
  3436.  
  3437.     my $userid = $self->valid_session_key__( $session );
  3438.     return undef if ( !defined( $userid ) );
  3439.  
  3440.     # Make sure that the bucket passed in actually exists
  3441.  
  3442.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket} ) ) {
  3443.         return 0;
  3444.     }
  3445.  
  3446.     $self->{db__}->do(                                                                        # PROFILE BLOCK START
  3447.         "delete from buckets where buckets.userid = $userid and buckets.name = '$bucket';" ); # PROFILE BLOCK STOP
  3448.     $self->db_update_cache__( $session );
  3449.  
  3450.     return 1;
  3451. }
  3452.  
  3453. #----------------------------------------------------------------------------
  3454. #
  3455. # rename_bucket
  3456. #
  3457. # Renames a bucket, returns 1 if the rename succeeded
  3458. #
  3459. # $session             A valid session key returned by a call to get_session_key
  3460. # $old_bucket          The old name of the bucket
  3461. # $new_bucket          The new name of the bucket
  3462. #
  3463. #----------------------------------------------------------------------------
  3464. sub rename_bucket
  3465. {
  3466.     my ( $self, $session, $old_bucket, $new_bucket ) = @_;
  3467.  
  3468.     my $userid = $self->valid_session_key__( $session );
  3469.     return undef if ( !defined( $userid ) );
  3470.  
  3471.     # Make sure that the bucket passed in actually exists
  3472.  
  3473.     if ( !defined( $self->{db_bucketid__}{$userid}{$old_bucket} ) ) {
  3474.         $self->log_( 0, "Bad bucket name $old_bucket to rename_bucket" );
  3475.         return 0;
  3476.     }
  3477.  
  3478.     my $id = $self->{db__}->quote( $self->{db_bucketid__}{$userid}{$old_bucket}{id} );
  3479.     $new_bucket = $self->{db__}->quote( $new_bucket );
  3480.  
  3481.     $self->log_( 1, "Rename bucket $old_bucket to $new_bucket" );
  3482.  
  3483.     my $result = $self->{db__}->do( "update buckets set name = $new_bucket where id = $id;" );
  3484.  
  3485.     if ( !defined( $result ) || ( $result == -1 ) ) {
  3486.         return 0;
  3487.     } else {
  3488.         $self->db_update_cache__( $session );
  3489.         return 1;
  3490.     }
  3491. }
  3492.  
  3493. #----------------------------------------------------------------------------
  3494. #
  3495. # add_messages_to_bucket
  3496. #
  3497. # Parses mail messages and updates the statistics in the specified bucket
  3498. #
  3499. # $session         A valid session key returned by a call to get_session_key
  3500. # $bucket          Name of the bucket to be updated
  3501. # @files           List of file names to parse
  3502. #
  3503. #----------------------------------------------------------------------------
  3504. sub add_messages_to_bucket
  3505. {
  3506.     my ( $self, $session, $bucket, @files ) = @_;
  3507.  
  3508.     my $userid = $self->valid_session_key__( $session );
  3509.     return undef if ( !defined( $userid ) );
  3510.  
  3511.     if ( !defined( $self->{db_bucketid__}{$userid}{$bucket}{id} ) ) {
  3512.         return 0;
  3513.     }
  3514.  
  3515.     # This is done to clear out the word list because in the loop
  3516.     # below we are going to not reset the word list on each parse
  3517.  
  3518.     $self->{parser__}->start_parse();
  3519.     $self->{parser__}->stop_parse();
  3520.  
  3521.     foreach my $file (@files) {
  3522.         $self->{parser__}->parse_file( $file,  # PROFILE BLOCK START
  3523.             $self->global_config_( 'message_cutoff'   ),
  3524.             0 );  # PROFILE BLOCK STOP (Do not reset word list)
  3525.     }
  3526.  
  3527.     $self->add_words_to_bucket__( $session, $bucket, 1 );
  3528.     $self->db_update_cache__( $session );
  3529.  
  3530.     return 1;
  3531. }
  3532.  
  3533. #----------------------------------------------------------------------------
  3534. #
  3535. # add_message_to_bucket
  3536. #
  3537. # Parses a mail message and updates the statistics in the specified bucket
  3538. #
  3539. # $session         A valid session key returned by a call to get_session_key
  3540. # $bucket          Name of the bucket to be updated
  3541. # $file            Name of file containing mail message to parse
  3542. #
  3543. #----------------------------------------------------------------------------
  3544. sub add_message_to_bucket
  3545. {
  3546.     my ( $self, $session, $bucket, $file ) = @_;
  3547.  
  3548.     my $userid = $self->valid_session_key__( $session );
  3549.     return undef if ( !defined( $userid ) );
  3550.  
  3551.     return $self->add_messages_to_bucket( $session, $bucket, $file );
  3552. }
  3553.  
  3554. #----------------------------------------------------------------------------
  3555. #
  3556. # remove_message_from_bucket
  3557. #
  3558. # Parses a mail message and updates the statistics in the specified bucket
  3559. #
  3560. # $session         A valid session key returned by a call to get_session_key
  3561. # $bucket          Name of the bucket to be updated
  3562. # $file            Name of file containing mail message to parse
  3563. #
  3564. #----------------------------------------------------------------------------
  3565. sub remove_message_from_bucket
  3566. {
  3567.     my ( $self, $session, $bucket, $file ) = @_;
  3568.  
  3569.     my $userid = $self->valid_session_key__( $session );
  3570.     return undef if ( !defined( $userid ) );
  3571.  
  3572.     $self->{parser__}->parse_file( $file,               # PROFILE BLOCK START
  3573.          $self->global_config_( 'message_cutoff'   ) ); # PROFILE BLOCK STOP
  3574.     $self->add_words_to_bucket__( $session, $bucket, -1 );
  3575.  
  3576.     $self->db_update_cache__( $session );
  3577.  
  3578.     return 1;
  3579. }
  3580.  
  3581. #----------------------------------------------------------------------------
  3582. #
  3583. # get_buckets_with_magnets
  3584. #
  3585. # Returns the names of the buckets for which magnets are defined
  3586. #
  3587. # $session     A valid session key returned by a call to get_session_key
  3588. #
  3589. #----------------------------------------------------------------------------
  3590. sub get_buckets_with_magnets
  3591. {
  3592.     my ( $self, $session ) = @_;
  3593.  
  3594.     my $userid = $self->valid_session_key__( $session );
  3595.     return undef if ( !defined( $userid ) );
  3596.  
  3597.     my @result;
  3598.  
  3599.     $self->{db_get_buckets_with_magnets__}->execute( $userid );
  3600.     while ( my $row = $self->{db_get_buckets_with_magnets__}->fetchrow_arrayref ) {
  3601.         push @result, ($row->[0]);
  3602.     }
  3603.  
  3604.     return @result;
  3605. }
  3606.  
  3607. #----------------------------------------------------------------------------
  3608. #
  3609. # get_magnet_types_in_bucket
  3610. #
  3611. # Returns the types of the magnets in a specific bucket
  3612. #
  3613. # $session     A valid session key returned by a call to get_session_key
  3614. # $bucket      The bucket to search for magnets
  3615. #
  3616. #----------------------------------------------------------------------------
  3617. sub get_magnet_types_in_bucket
  3618. {
  3619.     my ( $self, $session, $bucket ) = @_;
  3620.  
  3621.     my $userid = $self->valid_session_key__( $session );
  3622.     return undef if ( !defined( $userid ) );
  3623.  
  3624.     my @result;
  3625.  
  3626.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3627.     my $h = $self->{db__}->prepare( "select magnet_types.mtype from magnet_types, magnets, buckets
  3628.         where magnet_types.id = magnets.mtid and
  3629.               magnets.bucketid = buckets.id and
  3630.               buckets.id = $bucketid
  3631.               group by magnet_types.mtype
  3632.               order by magnet_types.mtype;" );
  3633.  
  3634.     $h->execute;
  3635.     while ( my $row = $h->fetchrow_arrayref ) {
  3636.         push @result, ($row->[0]);
  3637.     }
  3638.     $h->finish;
  3639.  
  3640.     return @result;
  3641. }
  3642.  
  3643. #----------------------------------------------------------------------------
  3644. #
  3645. # clear_bucket
  3646. #
  3647. # Removes all words from a bucket
  3648. #
  3649. # $session        A valid session key returned by a call to get_session_key
  3650. # $bucket         The bucket to clear
  3651. #
  3652. #----------------------------------------------------------------------------
  3653. sub clear_bucket
  3654. {
  3655.     my ( $self, $session, $bucket ) = @_;
  3656.  
  3657.     my $userid = $self->valid_session_key__( $session );
  3658.     return undef if ( !defined( $userid ) );
  3659.  
  3660.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3661.  
  3662.     $self->{db__}->do( "delete from matrix where matrix.bucketid = $bucketid;" );
  3663.     $self->db_update_cache__( $session );
  3664. }
  3665.  
  3666. #----------------------------------------------------------------------------
  3667. #
  3668. # clear_magnets
  3669. #
  3670. # Removes every magnet currently defined
  3671. #
  3672. # $session     A valid session key returned by a call to get_session_key
  3673. #
  3674. #----------------------------------------------------------------------------
  3675. sub clear_magnets
  3676. {
  3677.     my ( $self, $session ) = @_;
  3678.  
  3679.     my $userid = $self->valid_session_key__( $session );
  3680.     return undef if ( !defined( $userid ) );
  3681.  
  3682.     for my $bucket (keys %{$self->{db_bucketid__}{$userid}}) {
  3683.         my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3684.         $self->{db__}->do( "delete from magnets where magnets.bucketid = $bucketid" );
  3685.     }
  3686. }
  3687.  
  3688. #----------------------------------------------------------------------------
  3689. #
  3690. # get_magnets
  3691. #
  3692. # Returns the magnets of a certain type in a bucket
  3693. #
  3694. # $session         A valid session key returned by a call to get_session_key
  3695. # $bucket          The bucket to search for magnets
  3696. # $type            The magnet type (e.g. from, to or subject)
  3697. #
  3698. #----------------------------------------------------------------------------
  3699. sub get_magnets
  3700. {
  3701.     my ( $self, $session, $bucket, $type ) = @_;
  3702.  
  3703.     my $userid = $self->valid_session_key__( $session );
  3704.     return undef if ( !defined( $userid ) );
  3705.  
  3706.     my @result;
  3707.  
  3708.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3709.     my $h = $self->{db__}->prepare( "select magnets.val from magnets, magnet_types
  3710.         where magnets.bucketid = $bucketid and
  3711.               magnets.id != 0 and
  3712.               magnet_types.id = magnets.mtid and
  3713.               magnet_types.mtype = '$type' order by magnets.val;" );
  3714.  
  3715.     $h->execute;
  3716.     while ( my $row = $h->fetchrow_arrayref ) {
  3717.         push @result, ($row->[0]);
  3718.     }
  3719.     $h->finish;
  3720.  
  3721.     return @result;
  3722. }
  3723.  
  3724. #----------------------------------------------------------------------------
  3725. #
  3726. # create_magnet
  3727. #
  3728. # Make a new magnet
  3729. #
  3730. # $session         A valid session key returned by a call to get_session_key
  3731. # $bucket          The bucket the magnet belongs in
  3732. # $type            The magnet type (e.g. from, to or subject)
  3733. # $text            The text of the magnet
  3734. #
  3735. #----------------------------------------------------------------------------
  3736. sub create_magnet
  3737. {
  3738.     my ( $self, $session, $bucket, $type, $text ) = @_;
  3739.  
  3740.     my $userid = $self->valid_session_key__( $session );
  3741.     return undef if ( !defined( $userid ) );
  3742.  
  3743.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3744.     my $result = $self->{db__}->selectrow_arrayref("select magnet_types.id from magnet_types
  3745.                                                         where magnet_types.mtype = '$type';" );
  3746.  
  3747.     my $mtid = $result->[0];
  3748.  
  3749.     $text = $self->{db__}->quote( $text );
  3750.  
  3751.     $self->{db__}->do( "insert into magnets ( bucketid, mtid, val )
  3752.                                      values ( $bucketid, $mtid, $text );" );
  3753. }
  3754.  
  3755. #----------------------------------------------------------------------------
  3756. #
  3757. # get_magnet_types
  3758. #
  3759. # Get a hash mapping magnet types (e.g. from) to magnet names (e.g. From);
  3760. #
  3761. # $session     A valid session key returned by a call to get_session_key
  3762. #
  3763. #----------------------------------------------------------------------------
  3764. sub get_magnet_types
  3765. {
  3766.     my ( $self, $session ) = @_;
  3767.  
  3768.     my $userid = $self->valid_session_key__( $session );
  3769.     return undef if ( !defined( $userid ) );
  3770.  
  3771.     my %result;
  3772.  
  3773.     my $h = $self->{db__}->prepare( "select magnet_types.mtype, magnet_types.header from magnet_types order by mtype;" );
  3774.  
  3775.     $h->execute;
  3776.     while ( my $row = $h->fetchrow_arrayref ) {
  3777.         $result{$row->[0]} = $row->[1];
  3778.     }
  3779.     $h->finish;
  3780.  
  3781.     return %result;
  3782. }
  3783.  
  3784. #----------------------------------------------------------------------------
  3785. #
  3786. # delete_magnet
  3787. #
  3788. # Remove a new magnet
  3789. #
  3790. # $session         A valid session key returned by a call to get_session_key
  3791. # $bucket          The bucket the magnet belongs in
  3792. # $type            The magnet type (e.g. from, to or subject)
  3793. # $text            The text of the magnet
  3794. #
  3795. #----------------------------------------------------------------------------
  3796. sub delete_magnet
  3797. {
  3798.     my ( $self, $session, $bucket, $type, $text ) = @_;
  3799.  
  3800.     my $userid = $self->valid_session_key__( $session );
  3801.     return undef if ( !defined( $userid ) );
  3802.  
  3803.     my $bucketid = $self->{db_bucketid__}{$userid}{$bucket}{id};
  3804.     my $result = $self->{db__}->selectrow_arrayref("select magnet_types.id from magnet_types
  3805.                                                         where magnet_types.mtype = '$type';" );
  3806.  
  3807.     my $mtid = $result->[0];
  3808.  
  3809.     $self->{db__}->do( "delete from magnets
  3810.                             where magnets.bucketid = $bucketid and
  3811.                                   magnets.mtid = $mtid and
  3812.                                   magnets.val  = '$text';" );
  3813. }
  3814.  
  3815. #----------------------------------------------------------------------------
  3816. #
  3817. # get_stopword_list
  3818. #
  3819. # Gets the complete list of stop words
  3820. #
  3821. # $session     A valid session key returned by a call to get_session_key
  3822. #
  3823. #----------------------------------------------------------------------------
  3824. sub get_stopword_list
  3825. {
  3826.     my ( $self, $session ) = @_;
  3827.  
  3828.     my $userid = $self->valid_session_key__( $session );
  3829.     return undef if ( !defined( $userid ) );
  3830.  
  3831.     return $self->{parser__}->{mangle__}->stopwords();
  3832. }
  3833.  
  3834. #----------------------------------------------------------------------------
  3835. #
  3836. # magnet_count
  3837. #
  3838. # Gets the number of magnets that are defined
  3839. #
  3840. # $session     A valid session key returned by a call to get_session_key
  3841. #
  3842. #----------------------------------------------------------------------------
  3843. sub magnet_count
  3844. {
  3845.     my ( $self, $session ) = @_;
  3846.  
  3847.     my $userid = $self->valid_session_key__( $session );
  3848.     return undef if ( !defined( $userid ) );
  3849.  
  3850.     my $result = $self->{db__}->selectrow_arrayref( "select count(*) from magnets, buckets
  3851.         where buckets.userid = $userid and
  3852.               magnets.id != 0 and
  3853.               magnets.bucketid = buckets.id;" );
  3854.  
  3855.     if ( defined( $result ) ) {
  3856.         return $result->[0];
  3857.     } else {
  3858.         return 0;
  3859.     }
  3860. }
  3861.  
  3862. #----------------------------------------------------------------------------
  3863. #
  3864. # add_stopword, remove_stopword
  3865. #
  3866. # Adds or removes a stop word
  3867. #
  3868. # $session     A valid session key returned by a call to get_session_key
  3869. # $stopword    The word to add or remove
  3870. #
  3871. # Return 0 for a bad stop word, and 1 otherwise
  3872. #
  3873. #----------------------------------------------------------------------------
  3874. sub add_stopword
  3875. {
  3876.     my ( $self, $session, $stopword ) = @_;
  3877.  
  3878.     my $userid = $self->valid_session_key__( $session );
  3879.     return undef if ( !defined( $userid ) );
  3880.  
  3881.     # Pass language parameter to add_stopword()
  3882.  
  3883.     return $self->{parser__}->{mangle__}->add_stopword( $stopword, $self->module_config_( 'html', 'language' ) );
  3884. }
  3885.  
  3886. sub remove_stopword
  3887. {
  3888.     my ( $self, $session, $stopword ) = @_;
  3889.  
  3890.     my $userid = $self->valid_session_key__( $session );
  3891.     return undef if ( !defined( $userid ) );
  3892.  
  3893.     # Pass language parameter to remove_stopword()
  3894.  
  3895.     return $self->{parser__}->{mangle__}->remove_stopword( $stopword, $self->module_config_( 'html', 'language' ) );
  3896. }
  3897.  
  3898. #----------------------------------------------------------------------------
  3899. #----------------------------------------------------------------------------
  3900. # _____   _____   _____  _______ _____        _______   _______  _____  _____
  3901. #|_____] |     | |_____] |______   |   |      |______   |_____| |_____]   |
  3902. #|       |_____| |       |       __|__ |_____ |______   |     | |       __|__
  3903. #
  3904. #----------------------------------------------------------------------------
  3905. #----------------------------------------------------------------------------
  3906.  
  3907. # GETTERS/SETTERS
  3908.  
  3909. sub wordscores
  3910. {
  3911.     my ( $self, $value ) = @_;
  3912.  
  3913.     $self->{wordscores__} = $value if (defined $value);
  3914.     return $self->{wordscores__};
  3915. }
  3916.  
  3917. sub wmformat
  3918. {
  3919.     my ( $self, $value ) = @_;
  3920.  
  3921.     $self->{wmformat__} = $value if (defined $value);
  3922.     return $self->{wmformat__};
  3923. }
  3924.  
  3925. sub db
  3926. {
  3927.     my ( $self ) = @_;
  3928.  
  3929.     return $self->{db__};
  3930. }
  3931.  
  3932. sub history
  3933. {
  3934.     my ( $self, $history ) = @_;
  3935.  
  3936.     $self->{history__} = $history;
  3937. }
  3938.  
  3939. 1;
  3940.  
  3941.